diff --git a/Connect-CmRemoteTools.ps1 b/Connect-CmRemoteTools.ps1 new file mode 100644 index 0000000..5687e71 --- /dev/null +++ b/Connect-CmRemoteTools.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + This script calls the SCCM console's remote tools clients (CmRcViewer.exe) to start a SCCM + remote tools console from Powershell. +.NOTES + Created on: 12/9/2014 + Created by: Adam Bertram + Filename: Connect-CmRemoteTools.ps1 + Requirements: An available SCCM 2012 site server and the SCCM console installed + Permissions to connect to the remote computer +.EXAMPLE + PS> .\Connect-CmRemoteTools.ps1 -Computername MYCOMPUTER + + This example would bring up the SCCM remote tools console window connecting to the computer called MYCOMPUTER +.PARAMETER Computername + The name of the computer you'd like to use remote tools to connect to +.PARAMETER + The name of the SCCM site server holding the site database +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [ValidateScript({Test-Connection -ComputerName $_ -Quiet -Count 1})] + [string]$Computername, + [ValidateScript({ Test-Connection -ComputerName $_ -Quiet -Count 1 })] + [string]$SiteServer = 'CONFIGMANAGER' +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest +} + +process { + try { + ## Find the path of the admin console to get the path of the remote tools client + if (!$env:SMS_ADMIN_UI_PATH -or !(Test-Path "$($env:SMS_ADMIN_UI_PATH)\CmRcViewer.exe")) { + throw "Unable to find the SCCM remote tools exe. Is the console installed?" + } else { + $RemoteToolsFilePath = "$($env:SMS_ADMIN_UI_PATH)\CmRcViewer.exe" + } + + & $RemoteToolsFilePath $Computername "\\$SiteServer" + + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Convert-CMApplicationToPackage.ps1 b/Convert-CMApplicationToPackage.ps1 new file mode 100644 index 0000000..4c8bdc6 --- /dev/null +++ b/Convert-CMApplicationToPackage.ps1 @@ -0,0 +1,303 @@ +#Requires -Version 3 +#Requires -Module ConfigurationManager + +<# +.SYNOPSIS + This creates a ConfigMgr package/program for each deployment type from the attributes of a ConfigMgr application. +.DESCRIPTION + This reads a ConfigMgr application and gathers all needed attributes from the application itself and it's + deployment types. It then creates a package/program for each deployment type in the application. If more than one + deployment type exists the resulting package(s) will be named $ApplicationName - $DeploymentTypeName" with each + program being named $DeploymentTypeName. +.NOTES + Created on: 07/03/2014 + Created by: Adam Bertram + Filename: Convert-CMApplicationToPackage.ps1 + Credits: http://www.david-obrien.net/2014/01/24/convert-configmgr-applications-packages-powershell/ + Todos: If the app has a product code, use this for installation program management in the program + Create an option to distribute the package to DPs after creation +.DESCRIPTION + This gets all common attributes of a ConfigMgr application that a ConfigMgr package/program has and uses + these attributes to create a new packages with a program inside for each application deployment type. +.EXAMPLE + .\Convert-CMApplicationToPackage.ps1 -ApplicationName 'Application 1' + This example converts the application "Application 1" into a package called "Application 1" and a program + called "Install Application 1" if it has a single deployment type. +.EXAMPLE + .\Convert-CMApplicationToPackage.ps1 -ApplicationName 'Application 1' -SkipRequirements + This example converts the application "Application 1" into a package called "Application 1" and a program + called Install "Application 1" excluding disk space and OS requirements if it has a single deployment type. +.PARAMETER ApplicationName + This is the name of the application you'd like to convert. +.PARAMETER PackageName + This is the name of the package you'd like to create. If this param isn't used and only 1 deployment type + exists in the application, it will default to the name of the application else if the application has multiple + deployment types it will default to the application name and the name of the deployment type. +.PARAMETER SkipRequirement + Use this switch parameter if you don't want to bring over any disk or OS requirements from the application. +.PARAMETER DistributeContent + Use this swtich to find all DPs/DP Groups the application is distributed to and distribute the package to them after + the package has been created. +.PARAMETER OsdFriendlyPowershellSyntax + Use this switch parameter to convert any program that's simply a reference to a PS1 file that normally works + in a non-OSD environment to a full powershell syntax using powershell.exe. +.PARAMETER AdditionalOptions + An array of hashtables of any additional options that will be applied to the resulting package. Use the form + @(@{'Package' = @{ 'Property' = 'Value' } }) or @(@{'Program' = @{ 'Property' = 'Value' } }) +.PARAMETER SiteServerName + The ConfigMgr site server name +.PARAMETER SiteCode + The ConfigMgr site code. +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [string]$ApplicationName, + [string]$PackageName, + [switch]$SkipRequirements, + [switch]$DistributeContent, + [switch]$OsdFriendlyPowershellSyntax, + [array]$AdditionalOptions = @(@{'Package' = @{ 'PkgFlags' = '128' } }), + [string]$SiteServer = 'CONFIGMANAGER', + [string]$SiteCode = 'UHP' +) + +begin { + try { + ## This helper function gets all of the supported platform objects that's supported for creating OS requirements for a package, + ## looks for a match between each CI_UniqueID and the OS string and if there's a match, creates a new lazy property instance + ## populates the necessary values and returns an array of objects that can be used to populated the SupportOperatingSystemPlatforms + ## lazy WMI property on the SMS_Program object. + function New-SupportedOsObject([Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.Rule]$OsRequirement) { + $SupportedPlatforms = Get-WmiObject -ComputerName $SiteServer -Class SMS_SupportedPlatforms -Namespace "root\sms\site_$SiteCode" + $SupportedOs = @() + ## Define the array of OS strings to convert to objects + if ($OsRequirement.Expression.Operator.OperatorName -eq 'OneOf') { + $AppOsList = $OsRequirement.Expression.Operands.RuleId + } elseif ($OsRequirement.Expression.Operator.OperatorName -eq 'NoneOf') { + ## TODO: Query the site server for all possible operating system values and remove all OSes in + ## $OsRequirement.DeploymentTypes[0].Requirements[0].Expression.Operands.RuleId + return $false + } + foreach ($AppOs in $AppOsList) { + foreach ($OsDetail in $SupportedPlatforms) { + if ($AppOs -eq $OsDetail.CI_UniqueId) { + $instance = ([wmiclass]("\\$SiteServer\root\sms\site_$SiteCode`:SMS_OS_Details")).CreateInstance() + if ($instance -is [System.Management.ManagementBaseObject]) { + $instance.MaxVersion = $OsDetail.OSMaxVersion + $instance.MinVersion = $OsDetail.OSMinVersion + $instance.Name = $OsDetail.OSName + $instance.Platform = $OsDetail.OSPlatform + $SupportedOs += $instance + } + } + } + } + $SupportedOs + } + + function Convert-NalPathToName ($NalPath) { + $NalPath.Split('\\')[2].Split('.')[0] + } + + function Get-DpsinDpGroup ($GroupId) { + $Dps = Get-WmiObject @SiteServerWmiProps -Class SMS_DPGroupMembers -Filter "GroupID = '$GroupId'" + if ($Dps) { + $Dps.DPNALPath | foreach { Convert-NalPathToName $_ } + } else { + $false + } + } + + if (!(Test-Path "$(Split-Path $env:SMS_ADMIN_UI_PATH -Parent)\ConfigurationManager.psd1")) { + throw 'Configuration Manager module not found. Is the admin console intalled?' + } elseif (!(Get-Module 'ConfigurationManager')) { + Import-Module "$(Split-Path $env:SMS_ADMIN_UI_PATH -Parent)\ConfigurationManager.psd1" + } + $Location = (Get-Location).Path + Set-Location "$($SiteCode):" + + $Application = Get-CMApplication -Name $ApplicationName + if (!$Application) { + throw "$ApplicationName not found" + } + $ApplicationXML = [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::DeserializeFromString($Application.SDMPackageXML) + + $SetProgramProps = @{ } + + $SiteServerWmiProps = @{ + 'Computername' = $SiteServer; + 'Namespace' = "root\sms\site_$SiteCode" + } + + } catch { + Write-Error $_.Exception.Message + exit + } +} +process { + try { + $DeploymentTypes = $ApplicationXML.DeploymentTypes + + for ($i = 0; $i -lt $DeploymentTypes.Count; $i++) { + if ($DeploymentTypes.Count -gt 1) { + $PackageName = "$ApplicationName - $($ApplicationXML.DeploymentTypes[$i].Title)" + } elseif (!$PackageName) { + $PackageName = $ApplicationName + } + $ProgramName = $ApplicationXML.DeploymentTypes[$i].Title + + if (Get-CMPackage -Name $PackageName) { + throw "$PackageName already exists" + } + + $PackageProps = @{ + 'Name' = $PackageName; + 'Version' = $ApplicationXML.SoftwareVersion; + 'Manufacturer' = $ApplicationXML.Publisher; + 'Path' = $ApplicationXML.DeploymentTypes[$i].Installer.Contents.Location; + } + + ## 07/03/2014 - Even though the New-CMProgram documentation leads you to believe you can use a string for the RunType + ## param, it won't work. You must use the [Microsoft.ConfigurationManagement.Cmdlets.AppModel.Commands.RunType] object. + $NewProgramProps = @{ + 'StandardProgramName' = $ProgramName; + 'PackageName' = $PackageName; + 'RunType' = [Microsoft.ConfigurationManagement.Cmdlets.AppModel.Commands.RunType]::($ApplicationXML.DeploymentTypes[$i].Installer.UserInteractionMode) + } + + $AppCmdLine = $ApplicationXML.DeploymentTypes[$i].Installer.InstallCommandLine + ## If the command line is simply a reference to a single PS1 file + if ($OsdFriendlyPowershellSyntax.IsPresent -and ($AppCmdLine -match '.ps1$')) { + $NewProgramProps.CommandLine = "powershell.exe -ExecutionPolicy bypass -NoProfile -NoLogo -NonInteractive -File $AppCmdLine" + } else { + $NewProgramProps.CommandLine = $ApplicationXML.DeploymentTypes[$i].Installer.InstallCommandLine + } + + $SetProgramProps = @{ + 'EnableTaskSequence' = $true; + 'StandardProgramName' = $ProgramName; + 'Name' = $PackageName; + } + + ## 07/03/2014 - Due to a bug in the New-CMprogram cmdlet, even though 15 min or 720 min is allowed via the GUI + ## for the max run time, it doesn't work via the New-CMProgram cmdlet. To compensate, I'm adding or removing + ## 1 minute and this works. + $Duration = $ApplicationXML.DeploymentTypes[$i].Installer.MaxExecuteTime + if ($Duration -eq 15) { + $Duration = $Duration + 1 + } elseif ($Duration -eq 720) { + $Duration = $Duration - 1 + } + $NewProgramProps.Duration = $Duration + + if (!$SkipRequirements.IsPresent) { + $Requirements = $ApplicationXML.DeploymentTypes[$i].Requirements + $RequirementExpressions = $Requirements.Expression + $FreeSpaceRequirement = $RequirementExpressions | where { ($_.Operands.LogicalName -contains 'FreeDiskSpace') -and ($_.Operator.OperatorName -eq 'GreaterEquals') } + if ($FreeSpaceRequirement) { + $NewProgramProps.DiskSpaceRequirement = $FreeSpaceRequirement.Operands.value / 1MB + $NewProgramProps.DiskSpaceUnit = 'MB' + } + } + + switch ($ApplicationXML.DeploymentTypes[$i].Installer.RequiresLogon) { + $false { + $NewProgramProps.ProgramRunType = 'OnlyWhenNoUserIsLoggedOn' + } + $true { + $NewProgramProps.ProgramRunType = 'OnlyWhenUserIsLoggedOn' + } + default { + $NewProgramProps.ProgramRunType = 'WhetherOrNotUserIsLoggedOn' + } + } + + if ($ApplicationXML.DeploymentTypes[$i].Installer.UserInteractionMode -eq 'Hidden') { + $SetProgramProps['SuppressProgramNotifications'] = $true + } + + if ($ApplicationXML.DeploymentTypes[$i].Installer.SourceUpdateCode) { + ##TODO: Look into setting installation source management on the package + } + + $PostIntallBehavior = $ApplicationXML.DeploymentTypes[$i].Installer.PostInstallBehavior + if (($PostIntallBehavior -eq 'BasedOnExitCode') -or ($PostIntallBehavior -eq 'NoAction')) { + $SetProgramProps.AfterRunningType = 'NoActionRequired' + } elseif ($PostIntallBehavior -eq 'ProgramReboot') { + $SetProgramProps.AfterRunningType = 'ProgramControlsRestart' + } elseif ($PostIntallBehavior -eq 'ForceReboot') { + $SetProgramProps.AfterRunningType = 'ConfigurationManagerRestartsComputer' + } + + $NewPackage = New-CMPackage @PackageProps + Write-Verbose "Successfully created package name $($NewPackage.Name) ($($NewPackage.PackageID))" + $NewProgram = New-CMProgram @NewProgramProps + Set-CMProgram @SetProgramProps + + if (!$SkipRequirements.IsPresent) { + $OsRequirement = $Requirements | where { $_.Expression -is [Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.OperatingSystemExpression] } + if ($OsRequirement) { + $SupportedOs = New-SupportedOsObject $OsRequirement + $NewProgram.SupportedOperatingSystems = $SupportedOs + $NewProgram.Put() + } + } + + if ($AdditionalOptions) { + $AdditionalOptions | foreach { + $_.GetEnumerator() | foreach { + if ($_.Key -eq 'Package') { + $_.Value.GetEnumerator() | foreach { + $NewPackage.($_.Key) = $_.Value + $NewPackage.Put() + } + } elseif ($_.Key -eq 'Program') { + $_.Value.GetEnumerator() | foreach { + $NewProgram.($_.Key) = $_.Value + $NewProgram.Put() + } + } + } + } + } + + if ($DistributeContent.IsPresent) { + ## Distribute the converted package to all DP groups the application is a part of + $AllDpGroupPackages = Get-WmiObject @SiteServerWmiProps -Class SMS_DPGroupPackages + $AllDpGroups = Get-WmiObject @SiteServerWmiProps -Class SMS_DistributionPointGroup + + ## TODO: This currently doesn't support applications in multiple groups + $AppDpGroupId = ($AllDpGroupPackages | where { $_.PkgID -eq $Application.PackageID } | Group-Object GroupId).Name + $AppDpGroup = $AllDpGroups | where { $_.GroupID -eq $AppDpGroupId} + if ($AppDpGroup) { + Write-Verbose "Application is in a DP group" + Start-CMContentDistribution -DistributionPointGroupName $AppDpGroup.Name -PackageName $PackageName + $DpsInAppDpGroup = Get-DpsinDpGroup $AppDpGroupId + $SingleDps = Get-WmiObject @SiteServerWmiProps -Class SMS_DistributionPoint -Filter "SecureObjectID = '$($Application.ModelName)'" | where { $DpsInAppDpGroup -notcontains (Convert-NalPathToName $_.ServerNALPath) } + } else { + $SingleDps = Get-WmiObject @SiteServerWmiProps -Class SMS_DistributionPoint -Filter "SecureObjectID = '$($Application.ModelName)'" + } + + if ($SingleDps) { + Write-Verbose "Application is in $($SingleDps.Count) single DPs" + foreach ($Dp in $SingleDps) { + $DpName = Convert-DpNalPathToName $Dp.ServerNALPath + Write-Verbose "Adding package '$PackageName' to DP '$DpName'" + Start-CMContentDistribution -DistributionPoint $DpName -PackageName $PackageName + } + } + } + Unlock-CMObject $NewPackage + } + + } catch { + Write-Error "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + } +} + +end { + Set-Location $Location +} \ No newline at end of file diff --git a/Convert-DynamicDnsRecordToStatic.ps1 b/Convert-DynamicDnsRecordToStatic.ps1 new file mode 100644 index 0000000..a2f8dbb --- /dev/null +++ b/Convert-DynamicDnsRecordToStatic.ps1 @@ -0,0 +1,64 @@ +<# +.SYNOPSIS + +.NOTES + Created on: 8/22/2014 + Created by: Adam Bertram + Filename: + Credits: + Requirements: + Todos: +.EXAMPLE + +.EXAMPLE + +.PARAMETER PARAM1 + +.PARAMETER PARAM2 + +#> +[CmdletBinding(DefaultParameterSetName = 'name')] +[OutputType('System.Management.Automation.PSCustomObject')] +param ( + [Parameter(ParameterSetName = 'name', + Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [ValidateSet("Tom","Dick","Jane")] + [ValidateRange(21,65)] + [ValidateScript({Test-Path $_ -PathType 'Container'})] + [ValidateNotNullOrEmpty()] + [ValidateCount(1,5)] + [ValidateLength(1,10)] + [ValidatePattern()] + [string]$Computername = 'DEFAULTVALUE' +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + try { + + } catch { + Write-Error $_.Exception.Message + } +} + +process { + try { + Get-DnsServerResourceRecord -ComputerName dc01 -ZoneName hosp.uhhg.org | where { ($_.HostName -match '^U.*XA65') -and ($_.Hostname -notmatch 'VM') -and ($_.Hostname -notmatch '.hosp.uhhg.org$') } | select @{ n = 'Hostname'; e = { $_.Hostname } }, @{n = 'IpAddres s'; e = { $_.RecordData.IPv4Address.IPAddressToString } } + $CitrixRecords | select -Skip 1 | % { try { Add-DnsServerResourceRecord -ZoneName hosp.uhhg.org -ComputerName dc01 -IPv4Address $_.IpAddress -Name $_.Hostname -A } catch { } } + $CitrixRecords | % { Get-DnsServerResourceRecord -ComputerName dc01 -Name $_.Hostname -RRType A -ZoneName hosp.uhhg.org } + + } catch { + Write-Error $_.Exception.Message + } +} + +end { + try { + + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Copy-LocalPathToVmCdRom.ps1 b/Copy-LocalPathToVmCdRom.ps1 new file mode 100644 index 0000000..cc30be5 --- /dev/null +++ b/Copy-LocalPathToVmCdRom.ps1 @@ -0,0 +1,199 @@ +<# +.SYNOPSIS + This creates an ISO file based on a specified path, uploads this ISO file to a VMware + datastore and mounts the ISO file on a VM. +.NOTES + Created on: 7/14/2014 + Created by: Adam Bertram + Filename: Copy-LocalPathToVmCdRom.ps1 +.EXAMPLE + .\Copy-LocalPathToVmRdRom.ps1 -Folderpath c:\folder -Vm 'VM1' -DatatoreFolder 'VM1 Folder' + + This creates an ISO file from the contents of C:\folder, copies this ISO file to the + 'VM1 folder' datastore folder and mounts it to the VM 'VM1' +.PARAMETER FolderPath + The folder path containing the files you'd like transfer to the VM +.PARAMETER Vm + The name of the VMware virtual machine +.PARAMETER Datacenter + The name of the Vcenter datacenter the VM is located on +.PARAMETER Datastore + The name of the datastore the VM is located on +.PARAMETER DatastoreFolder + The name of the datastore folder you'd like to put the ISO file into +.PARAMETER Force + To remove any existing ISO on the datastore with the same name as the folder you're copying. + Without this parameter, if an existing ISO is found, the action will fail. + +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [ValidateScript({Test-Path $_ -PathType 'Container'})] + [string]$FolderPath, + [Parameter(Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [string]$Vm, + [Parameter(ValueFromPipelineByPropertyName)] + [string]$Datacenter = 'Development', + [Parameter(ValueFromPipelineByPropertyName)] + [string]$Datastore = 'ruby02-localdatastore01', + [Parameter(Mandatory, + ValueFromPipelineByPropertyName)] + [string]$DatastoreFolder, + [switch]$Force +) + +begin { + Set-StrictMode -Version Latest + try { + if (!(Get-PSSnapin 'VMware.VimAutomation.Core')) { + throw 'PowerCLI snapin is not available' + } + $VmObject = Get-VM $Vm -ErrorAction SilentlyContinue + if (!$VmObject) { + throw "VM $Vm does not exist on connected VI server" + } + + if ($VmObject.PowerState -ne 'PoweredOn') { + throw "VM $Vm is not powered on. Cannot change CD-ROM IsoFilePath" + } + + $ExistingCdRom = $VmObject | Get-CDDrive + if (!$ExistingCdRom.ConnectionState.Connected) { + throw 'No CD-ROM attached. VM is powered on so I cannot attach a new one' + } + + $TempIsoName = "$($Folderpath | Split-Path -Leaf).iso" + $DatastoreIsoFolderPath = "vmstore:\$DataCenter\$Datastore\$DatastoreFolder" + if (Test-Path "$DatastoreIsoFolderPath\$TempIsoName") { + if ($Force) { + throw "-Force currently in progress. ISO file $DatastoreIsoFolderPath\$TempIsoName already exists in datastore" + ## Remove current ISO CDROM from VM + + ## Remove ISO from datastore folder + } else { + throw "ISO file $DatastoreIsoFolderPath\$TempIsoName already exists in datastore" + } + } + + ## Hide the PowerCLI progres bars + $ProgressPreference = 'SilentlyContinue' + function New-IsoFile { + <# + .Synopsis + Creates a new .iso file + .Description + The New-IsoFile cmdlet creates a new .iso file containing content from chosen folders + .Example + New-IsoFile "c:\tools","c:Downloads\utils" + Description + ----------- + This command creates a .iso file in $env:temp folder (default location) that contains c:\tools and c:\downloads\utils folders. The folders themselves are added in the root of the .iso image. + .Example + dir c:\WinPE | New-IsoFile -Path c:\temp\WinPE.iso -BootFile etfsboot.com -Media DVDPLUSR -Title "WinPE" + Description + ----------- + This command creates a bootable .iso file containing the content from c:\WinPE folder, but the folder itself isn't included. Boot file etfsboot.com can be found in Windows AIK. Refer to IMAPI_MEDIA_PHYSICAL_TYPE enumeration for possible media types: + + http://msdn.microsoft.com/en-us/library/windows/desktop/aa366217(v=vs.85).aspx + .Notes + NAME: New-IsoFile + AUTHOR: Chris Wu + LASTEDIT: 03/06/2012 14:06:16 + #> + Param ( + [parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]$Source, + [parameter(Position = 1)][string]$Path = "$($env:temp)\" + (Get-Date).ToString("yyyyMMdd-HHmmss.ffff") + ".iso", + [string] $BootFile = $null, + [string] $Media = "Disk", + [string] $Title = (Get-Date).ToString("yyyyMMdd-HHmmss.ffff"), + [switch] $Force + )#End Param + + Begin { + ($cp = new-object System.CodeDom.Compiler.CompilerParameters).CompilerOptions = "/unsafe" + if (!("ISOFile" -as [type])) { + Add-Type -CompilerParameters $cp -TypeDefinition @" + public class ISOFile + { + public unsafe static void Create(string Path, object Stream, int BlockSize, int TotalBlocks) + { + int bytes = 0; + byte[] buf = new byte[BlockSize]; + System.IntPtr ptr = (System.IntPtr)(&bytes); + System.IO.FileStream o = System.IO.File.OpenWrite(Path); + System.Runtime.InteropServices.ComTypes.IStream i = Stream as System.Runtime.InteropServices.ComTypes.IStream; + + if (o == null) { return; } + while (TotalBlocks-- > 0) { + i.Read(buf, BlockSize, ptr); o.Write(buf, 0, bytes); + } + o.Flush(); o.Close(); + } + } +"@ + }#End If + + if ($BootFile -and (Test-Path $BootFile)) { + ($Stream = New-Object -ComObject ADODB.Stream).Open() + $Stream.Type = 1 # adFileTypeBinary + $Stream.LoadFromFile((Get-Item $BootFile).Fullname) + ($Boot = New-Object -ComObject IMAPI2FS.BootOptions).AssignBootImage($Stream) + }#End If + + $MediaType = @{ + CDR = 2; CDRW = 3; DVDRAM = 5; DVDPLUSR = 6; DVDPLUSRW = 7; ` + DVDPLUSR_DUALLAYER = 8; DVDDASHR = 9; DVDDASHRW = 10; DVDDASHR_DUALLAYER = 11; ` + DISK = 12; DVDPLUSRW_DUALLAYER = 13; BDR = 18; BDRE = 19 + } + + if ($MediaType[$Media] -eq $null) { write-debug "Unsupported Media Type: $Media"; write-debug ("Choose one from: " + $MediaType.Keys); break } + ($Image = new-object -com IMAPI2FS.MsftFileSystemImage -Property @{ VolumeName = $Title }).ChooseImageDefaultsForMediaType($MediaType[$Media]) + + if ((Test-Path $Path) -and (!$Force)) { "File Exists $Path"; break } + New-Item -Path $Path -ItemType File -Force | Out-Null + if (!(Test-Path $Path)) { + "cannot create file $Path" + break + } + } + + Process { + switch ($Source) { { $_ -is [string] } { $Image.Root.AddTree((Get-Item $_).FullName, $true) | Out-Null; continue } + { $_ -is [IO.FileInfo] } { $Image.Root.AddTree($_.FullName, $true); continue } + { $_ -is [IO.DirectoryInfo] } { $Image.Root.AddTree($_.FullName, $true); continue } + }#End switch + }#End Process + + End { + $Result = $Image.CreateResultImage() + [ISOFile]::Create($Path, $Result.ImageStream, $Result.BlockSize, $Result.TotalBlocks) + }#End End + }#End function New-IsoFile + + + } catch { + Write-Error $_.Exception.Message + exit + } +} + +process { + try { + ## Create an ISO + $IsoFilePath = "$($env:TEMP)\$TempIsoName" + Get-ChildItem $FolderPath | New-IsoFile -Path $IsoFilePath -Title ($Folderpath | Split-Path -Leaf) -Force + ## Upload the ISO to the datastore + $Iso = Copy-DatastoreItem $IsoFilePath "vmstore:\$Datacenter\$Datastore\$DatastoreFolder" -PassThru + ## Attach the ISO to the VM + $VmObject | Get-CDDrive | Set-CDDrive -IsoPath $Iso.DatastoreFullPath -Connected $true -Confirm:$false | Out-Null + ## Delete the temp ISO + Remove-Item $IsoFilePath -Force + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Cowbell.wav b/Cowbell.wav new file mode 100644 index 0000000..891a8de Binary files /dev/null and b/Cowbell.wav differ diff --git a/Create-AzureVMSnapshot.ps1 b/Create-AzureVMSnapshot.ps1 new file mode 100644 index 0000000..46028ec --- /dev/null +++ b/Create-AzureVMSnapshot.ps1 @@ -0,0 +1,122 @@ +<# +.SYNOPSIS + This creates a blob copy (snapshot) of all disks used by an Azure VM +.NOTES + Created on: 7/5/2014 + Created by: Adam Bertram + Filename: Create-AzureVmSnapshot.ps1 + Credits: http://bit.ly/1ouJKjR + Requirements: Azure IaaS VM +.EXAMPLE + Create-AzureVmSnapshot -AzureVM CCM1 -ServiceName CLOUD + This example will create a blob copy of all disks in the VM CCM1, Service name CLOUD +.EXAMPLE + Create-AzureVmSnapshot -AzureVM 'CCM1','CCM2' -Overwrite + This example creates blob copies for all disks in the VMs CCM1 and CCM2 using the default + service name parameter and if any existing copies are detected, automatically overwrite them. +.PARAMETER AzureVM + The name of the Azure VM. Multiple VM names are supported. +.PARAMETER ServiceName + The name of your Azure cloud service. +.PARAMETER Overwrite + Use this switch to overwrite any existing blob copies without asking +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory, + ValueFromPipeline)] + [string[]]$AzureVM, + [string]$ServiceName = 'ADBCLOUD', + [switch]$Overwrite +) + +begin { + Set-StrictMode -Version Latest + try { + $AzureModuleFilePath = "$($env:ProgramFiles)\Microsoft SDKs\Windows Azure\PowerShell\ServiceManagement\Azure\Azure.psd1" + if (!(Test-Path $AzureModuleFilePath)) { + Write-Error 'Azure module not found' + } else { + Import-Module $AzureModuleFilePath + } + + $script:BackupContainer = 'backups' + + function New-Snapshot([Microsoft.WindowsAzure.Commands.ServiceManagement.Model.PersistentVMModel.OSVirtualHardDisk]$Disk) { + $Blob = $Disk.MediaLink.Segments[-1] + $Container = $Disk.MediaLink.Segments[-2].TrimEnd('/') + $BlobCopyParams = @{ + 'SrcContainer' = $Container; + 'SrcBlob' = $Blob; + 'DestContainer' = $BackupContainer + } + if ($Overwrite.IsPresent) { + $BlobCopyParams.Force = $true + } + Start-AzureStorageBlobCopy @BlobCopyParams + #Get-AzureStorageBlobCopyState -Container $BackupContainer -Blob $Blob -WaitForComplete + } + + ## Ensure the container is created to store the snapshot + if (!(Get-AzureStorageContainer -Name $BackupContainer -ea SilentlyContinue)) { + Write-Verbose "Container $BackupContainer not found. Creating..." + New-AzureStorageContainer -Name $BackupContainer -Permission Off + } + + } catch { + Write-Error $_.Exception.Message + exit + } +} + +process { + try { + foreach ($Vm in $AzureVM) { + $Vm = Get-AzureVM -ServiceName $ServiceName -Name $Vm + if ($Vm.Status -ne 'StoppedVM') { + if ($Vm.Status -eq 'ReadyRole') { + Write-Verbose "VM $($Vm.Name) is started. Bringing down into a provisioned state" + ## Bring the VM down in a provisioned state + $Vm | Stop-AzureVm -StayProvisioned + } elseif ($Vm.Status -eq 'StoppedDeallocated') { + Write-Verbose "VM $($Vm.Name) is stopped but not in a provisioned state." + ## Bring up the VM and bring it back down in a provisioned state + Write-Verbose "Starting up VM $($Vm.Name)..." + $Vm | Start-AzureVm + while ((Get-AzureVm -ServiceName $ServiceName -Name $Vm.Name).Status -ne 'ReadyRole') { + sleep 5 + Write-Verbose "Waiting on VM $($Vm.Name) to be in a ReadyRole state..." + } + Write-Verbose "VM $($Vm.Name) now up. Bringing down into a provisioned state..." + $Vm | Stop-AzureVm -StayProvisioned + } + + } + + $OsDisk = $Vm | Get-AzureOSDisk + Get-AzureSubscription | Set-AzureSubscription -CurrentStorageAccountName ($OsDisk.MediaLink.Host.Split('.')[0]) + + ## Take snapshot of OS disk + New-Snapshot -Disk $OsDisk + + ## Take snapshots of all data disks + $DataDisks = $Vm | Get-AzureDataDisk + if ($DataDisks) { + foreach ($DataDisk in $DataDisks) { + New-Snapshot -Disk $DataDisk + } + } + } + } catch { + Write-Error $_.Exception.Message + exit + } +} + +end { + try { + + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Create-CMRandomMemberCollection.ps1 b/Create-CMRandomMemberCollection.ps1 new file mode 100644 index 0000000..c332b1a --- /dev/null +++ b/Create-CMRandomMemberCollection.ps1 @@ -0,0 +1,126 @@ +<# +.SYNOPSIS + This script adds a random sampling of online clients from another ConfigMgr collection +.DESCRIPTION + This script is useful for creating test collections for testing software deployments. It pings a random sampling of clients from + one collection and adds them into another collection. +.EXAMPLE + .\CreateRandomMemberConfigMgrCollection.ps1 -Server CONFIGMANAGER -SourceCollectionName COLLECTIONNAME -DestinationCollectionName COLLECTIONNAME -NumberOfClients 30 +.PARAMETER Server + The ConfigMgr site server name +.PARAMETER SourceCollectionName + The name of the ConfigMgr collection that will be used to sample clients from +.PARAMETER DestinationCollectionName + The name of the ConfigMgr collection that will be used (or created if doesn't exist) to place the random sampling of clients +.PARAMETER NumberOfClients + This is how many clients are taken at random from the source collection to make the destination collection's members +.NOTES + Revisions: 06/11/2014 + - filtered $SourceClients by only including OSes 'Microsoft Windows NT' + - modified the way a random client is chosen +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory = $False, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $False, + HelpMessage = 'What site server would you like to connect to?')] + [string]$Server = 'CONFIGMANAGER', + [Parameter(Mandatory = $False, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $False, + HelpMessage = 'What site does your ConfigMgr site server exist in?')] + [string]$Site = 'UHP', + [Parameter(Mandatory = $True, + ValueFromPipeline = $True, + ValueFromPipelineByPropertyName = $True, + HelpMessage = 'What device collection would you like to get clients from?')] + [string]$SourceCollectionName, + [Parameter(Mandatory = $True, + ValueFromPipeline = $True, + ValueFromPipelineByPropertyName = $True, + HelpMessage = 'What device collection would you like to put the random clients into?')] + [string]$DestinationCollectionName, + [Parameter(Mandatory = $True, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $True, + HelpMessage = 'How many random clients would you like to put into the destination collection?')] + [int]$NumberOfClients +) + +begin { + try { + ## I capture the drive path first because I have to switch to the ConfigMgr provider drive + ## to use the ConfigMgr cmdlets. I will change the drive path to the original after the + ## script is complete. + $BeforeLocation = (Get-Location).Path + Set-Location "$Site`:" + + $ConfigMgrWmiProps = @{ + 'ComputerName' = $Server; + 'Namespace' = "root\sms\site_$Site" + } + + Write-Verbose 'Verifying parameters...' + if (!(Get-CmDeviceCollection -Name $SourceCollectionName)) { + throw "$SourceCollectionName does not exist" + } elseif (!(Get-CmDeviceCollection -Name $DestinationCollectionName)) { + throw "$DestinationCollectionName does not exist" + } + $SourceCollectionId = (Get-WmiObject @ConfigMgrWmiProps -Class SMS_Collection -Filter "Name = '$SourceCollectionName'").CollectionId + $DestinationCollectionId = (Get-WmiObject @ConfigMgrWmiProps -Class SMS_Collection -Filter "Name = '$DestinationCollectionName'").CollectionId + $SourceClients = Get-WmiObject @ConfigMgrWmiProps -Class "SMS_CM_RES_COLL_$SourceCollectionId" | where { $_.DeviceOS -match 'Microsoft Windows NT' } | select Name, DeviceOS | group DeviceOS | sort count -Descending + $ExistingDestinationClients = Get-WmiObject @ConfigMgrWmiProps -Class "SMS_CM_RES_COLL_$DestinationCollectionId" | select -ExpandProperty Name + if ($SourceClients.Count -eq 0) { + throw 'Source collection does not contain any members' + } + $TargetClients = @() + } catch { + ## TODO: Ensure this breaks out of the entire script rather than just the BEGIN block + Write-Error $_.Exception.Message + } +} + +process { + try { + if ($NumberOfClients -lt $SourceClients.Count) { + Write-Verbose "Number of clients needed ($NumberOfClients) are less than total number of operating system groups ($($SourceClients.Count))..." + ## Find the OSes that have the highest count and get random clients from each of those + ## $SourceClients was sorted desc above so I know the lowest array indexes have the highest counts + $OsGroups = $SourceClients[0..($NumberOfClients - 1)] + } else { + Write-Verbose "Number of clients needed ($NumberOfClients) are equal to or exceed total operating system groups ($($SourceClients.Count))..." + $OsGroups = $SourceClients + } + Write-Verbose "Total OS groupings: $($OsGroups.Count)" + + ## TODO: What if the number of clients needed is greater than the online source clients? + for ($i = 0; $TargetClients.Count -lt $NumberOfClients; $i++) { + $GroupIndex = $i % $OsGroups.Count + Write-Verbose "Using group index $GroupIndex..." + $OsGroup = $OsGroups[$GroupIndex].Group + Write-Verbose "Using group $($OsGroups[$GroupIndex].Name)..." + $ClientName = ($OsGroups[$GroupIndex].Group | Get-Random).Name + Write-Verbose "Testing $ClientName for validity to add to target collection..." + if (($TargetClients -notcontains $ClientName) -and ($ExistingDestinationClients -notcontains $ClientName) -and (Test-Ping $ClientName)) { + Write-Verbose "$ClientName found to be acceptable. Adding to target collection array..." + $TargetClients += $ClientName + } + } + Write-Verbose 'Finished checking clients. Begin adding to target collection...' + + ## The TargetClients array is necessary instead of directly adding membership rules in the for loop + ## before if not, I couldn't ensure dups aren't added + $TargetClients | + foreach { + Write-Verbose "Adding $($_) to target collection $DestinationCollectionName..." + Add-CMDeviceCollectionDirectMembershipRule -CollectionName $DestinationCollectionName -ResourceId (Get-CmDevice -Name $_).ResourceID + } + } catch { + Write-Error $_.Exception.Message + } +} + +end { + Set-Location $BeforeLocation +} \ No newline at end of file diff --git a/DownloadQualysPatches.ps1 b/DownloadQualysPatches.ps1 new file mode 100644 index 0000000..f9cc5d8 --- /dev/null +++ b/DownloadQualysPatches.ps1 @@ -0,0 +1,17 @@ +param($scan_results_file) +$download_path = 'C:\Documents and Settings\abertram\Desktop\CONFIGMANAGER Vulnerabilities' + +$mht_contents = gc $scan_results_file +$ie = New-Object -ComObject 'InternetExplorer.Application' +$webclient = New-Object System.Net.WebClient +$ie.Navigate2('file:///C:\Documents and Settings\abertram\Desktop\CONFIGMANAGER Vulnerabilities\Scan_Results_unnhs_tw1_20131121_scan_1385046170_57428.mht') + +$links = $ie.Document.getElementsByTagName('a') | ? {($_.innertext -like '*Windows Server 2012*') -and ($_.innertext -notlike '*Core*') -and ($_.href -like 'http://*')} | select -ExpandProperty href +foreach ($link in $links) { + $r = Invoke-WebRequest -Uri $link -UseBasicParsing + $r.Links | ? {$_.outerhtml -like '*Download*'} | % { + $_.href + $y = Invoke-WebRequest "http://www.microsoft.com$($_.href)" -UseBasicParsing + } + $y.Links | ? {$_.outerhtml -like '*Click Here*'} | select -first 1 | % {$webclient.DownloadFile($_.href,"$download_path\$(split-path $_.href -Leaf)")} +} diff --git a/Enable-RemotePSRemoting.ps1 b/Enable-RemotePSRemoting.ps1 new file mode 100644 index 0000000..ff08731 --- /dev/null +++ b/Enable-RemotePSRemoting.ps1 @@ -0,0 +1,78 @@ +<# +.SYNOPSIS + +.NOTES + Created on: 5/9/2014 3:46 PM + Created by: Adam Bertram + Organization: + Filename: +.DESCRIPTION + +.EXAMPLE + +.EXAMPLE + +.PARAMETER Computername + The remote computer to enable PS remoting on + +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory = $True, + ValueFromPipeline = $True, + ValueFromPipelineByPropertyName = $True)] + [string]$Computername, + [Parameter(Mandatory = $False, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $False)] + [string]$PsExecPath = 'C:\PsExec.exe' +) + +begin { + ## http://www.leeholmes.com/blog/2009/11/20/testing-for-powershell-remoting-test-psremoting/ + function Test-PsRemoting { + param ( + [Parameter(Mandatory = $true)] + $computername + ) + + try { + $errorActionPreference = "Stop" + $result = Invoke-Command -ComputerName $computername { 1 } + } catch { + return $false + } + + ## I’ve never seen this happen, but if you want to be + ## thorough…. + if ($result -ne 1) { + Write-Verbose "Remoting to $computerName returned an unexpected result." + return $false + } + $true + } + + if (!(Test-Ping $Computername)) { + throw 'Computer is not reachable' + } elseif (!(Test-Path $PsExecPath)) { + throw 'Psexec.exe not found' + } +} + +process { + if (Test-PsRemoting $Computername) { + Write-Warning "Remoting already enabled on $Computername" + } else { + Write-Verbose "Attempting to enable remoting on $Computername..." + & $PsExecPath "\\$Computername" -s c:\windows\system32\winrm.cmd quickconfig -quiet + if (!(Test-PsRemoting $Computername)) { + Write-Warning "Remoting was attempted but not enabled on $Computername" + } else { + Write-Verbose "Remoting successfully enabled on $Computername" + } + } +} + +end { + +} \ No newline at end of file diff --git a/Find-WmiClass.ps1 b/Find-WmiClass.ps1 new file mode 100644 index 0000000..4a5ed73 --- /dev/null +++ b/Find-WmiClass.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS + This script finds a WMI class by name in all namespaces on a compputer +.NOTES + Created on: 1/29/15 + Created by: Adam Bertram + Filename: Find-WmiClass.ps1 +.EXAMPLE + PS> Find-WmiClas.ps1 -ClassSearchString 'MatchThis' -Computername 'MYSERVER' + + This example will recursively query all WMI namespaces on the computer MYSERVER and attempt to find any + CIM class that is the exact value of 'MatchThis' +.EXAMPLE + PS> Find-WmiClas.ps1 -Computername 'MYSERVER' + + This example will recursively query all WMI namespaces on the computer MYSERVER and return all WMI classes +.EXAMPLE + PS> Find-WmiClas.ps1 -ClassSearchString '*Wildcard*' -Computername 'MYSERVER' + + This example will recursively query all WMI namespaces on the computer MYSERVER and and attempt to find any + CIM class that matches the value of '*WildCard*' +.PARAMETER PARAM1 + +.PARAMETER PARAM2 + +#> +[CmdletBinding()] +param ( + [string]$ClassSearchString = '*', + [string]$PropertySearchString = '*', + [string]$Namespace = 'root', + [ValidateScript({ Test-Connection -ComputerName $_ -Quiet -Count 1 })] + [string]$Computername = 'localhost' +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + function Get-Namespace ($Namespace = 'root',[switch]$Recursive) { + try { + $Ns = Get-WmiObject -Computername $Computername -Namespace $Namespace -Class '__Namespace' -ea 'SilentlyContinue' | Select-Object -ExpandProperty Name + if (!$Ns) { + $false + } else { + foreach ($n in $Ns) { + try { + $Ns = "$Namespace\$n" + if ($Recursive.IsPresent) { + Get-Namespace -Namespace $Ns -Recursive + } else { + $Ns + } + } catch { + Write-Error $_.Exception.Message + } + } + } + } catch { + Write-Error $_.Exception.Message + $false + } + } +} + +process { + $Namespaces = @() + $Namespaces += Get-Namespace -Namespace $Namespace -Recursive; + if ($Namespaces) { + $Namespaces += $Namespace + foreach ($Ns in $Namespaces) { + try { + $Classes = Get-CimClass -Computername $Computername -Namespace $Ns -ClassName $ClassSearchString -ea SilentlyContinue + if ($Classes) { + foreach ($Class in $Classes) { + if ($PropertySearchString -eq '*') { + [pscustomobject]@{ 'Computername' = $Computername; 'Namespace' = $Ns; 'Class' = $Class.CimClassName } + } else { + $Properties = $Class.CimClassProperties + if ($Properties.Count -gt 0) { + foreach ($Prop in ($Properties.Name | where { $_ -like $PropertySearchString })) { + try { + $Value = Get-Wmiobject -Computername $Computername -Namespace $Ns -Class $Class.CimClassName -ea silentlycontinue | select -ExpandProperty $Prop + [pscustomobject]@{ 'Computername' = $Computername; 'Namespace' = $Ns; 'Class' = $Class.CimClassName; 'Property' = $Prop; 'Value' = $Value } + } catch { + + } + } + } + [pscustomobject]@{ 'Computername' = $Computername; 'Namespace' = $Ns; 'Class' = $Class.CimClassName; } + } + } + } + } catch { + Write-Error "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + } + } + } +} \ No newline at end of file diff --git a/Get-AdDnsRecordAcl.ps1 b/Get-AdDnsRecordAcl.ps1 new file mode 100644 index 0000000..04e5a35 --- /dev/null +++ b/Get-AdDnsRecordAcl.ps1 @@ -0,0 +1,60 @@ +#Requires -Module ActiveDirectory + +<# +.SYNOPSIS + This script retrieves the ACL from an Active Directory-integrated DNS record +.NOTES + Created on: 8/5/2014 + Created by: Adam Bertram + Filename: Get-AdDnsRecordAcl.ps1 +.EXAMPLE + PS> .\Get-AdDnsRecordAcl.ps1 -Hostname 'SERVER1' + + This example retrieves the ACL for the hostname SERVER1 inside the current forest-integrated + DNS zone inside Active Directory +.EXAMPLE + PS> .\Get-AdDnsRecordAcl.ps1 -Hostname 'SERVER1' -AdDnsIntegration 'Domain' + + This example retrieves the ACL for the hostname SERVER1 inside the current domain-integrated + DNS zone inside Active Directory +.PARAMETER Hostname + The hostname for the DNS record you'd like to see +.PARAMETER DomainName + The Active Directory domain name. This defaults to the current domain +.PARAMETER AdDnsIntegration + This is the DNS integration type. This can either be Forest and Domain. + +#> +[CmdletBinding()] +[OutputType('System.DirectoryServices.ActiveDirectorySecurity')] +param ( + [Parameter(Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [string[]]$Hostname, + [Parameter(ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [string]$DomainName = (Get-ADDomain).Forest, + [ValidateSet('Forest','Domain')] + [Parameter(ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [string[]]$AdDnsIntegration = 'Forest' +) + +begin { + $ErrorActionPreference = 'Stop' + Set-StrictMode -Version Latest +} + +process { + try { + $Path = "AD:\DC=$DomainName,CN=MicrosoftDNS,DC=$AdDnsIntegration`DnsZones,DC=$($DomainName.Split('.') -join ',DC=')" + foreach ($Record in (Get-ChildItem -Path $Path)) { + if ($Hostname -contains $Record.Name) { + Get-Acl -Path "ActiveDirectory:://RootDSE/$($Record.DistinguishedName)" + } + } + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Get-AdUserMatches.ps1 b/Get-AdUserMatches.ps1 new file mode 100644 index 0000000..d3b8243 --- /dev/null +++ b/Get-AdUserMatches.ps1 @@ -0,0 +1,373 @@ +<# +.SYNOPSIS + +.NOTES + Created on: 8/22/2014 + Created by: Adam Bertram + Filename: + Credits: + Requirements: + Todos: +.EXAMPLE + +.EXAMPLE + +.PARAMETER PARAM1 + +.PARAMETER PARAM2 + +#> +[CmdletBinding(DefaultParameterSetName = 'CSV')] +[OutputType('System.Management.Automation.PSCustomObject')] +param ( + [hashtable]$AdToSourceFieldMappings = @{ 'givenName' = 'FirstName'; 'Initials' = 'MiddleInitial'; 'surName' = 'LastName' }, + [hashtable]$AdToOutputFieldMappings = @{ 'givenName' = 'AD First Name'; 'Initials' = 'AD Middle Initial'; 'surName' = 'AD Last Name'; 'samAccountName' = 'AD Username' }, + [ValidateScript({Test-Path -Path $_ -PathType 'Leaf'})] + [Parameter(Mandatory, ParameterSetName = 'CSV')] + [ValidateScript({Test-Path -Path $_ -PathType 'Leaf' })] + [string]$CsvFilePath +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + try { + #region MatchTests + function Test-MatchFirstNameLastName ($FirstName, $LastName) { + if ($FirstName -and $LastName) { + Write-Verbose -Message "$($MyInvocation.MyCommand) - Searching for AD user match based on first name '$FirstName', last name '$LastName'" + $Match = $AdUsers | where { ($_.givenName -eq $FirstName) -and ($_.surName -eq $LastName) } + if ($Match) { + Write-Verbose "$($MyInvocation.MyCommand) - Match(es) found!" + $Match + } else { + Write-Verbose "$($MyInvocation.MyCommand) - Match not found" + $false + } + } else { + Write-Verbose "$($MyInvocation.MyCommand) - Match not found. Either first or last name is null" + $false + } + } + + function Test-MatchFirstNameMiddleInitialLastName ($FirstName, $MiddleInitial, $LastName) { + if ($FirstName -and $LastName -and $MiddleInitial) { + Write-Verbose -Message "$($MyInvocation.MyCommand) - Searching for AD user match based on first name '$FirstName', middle initial '$MiddleInitial' and last name '$LastName'" + $Match = $AdUsers | where { ($_.givenName -eq $FirstName) -and ($_.surName -eq $LastName) -and (($_.Initials -eq $MiddleInitial) -or ($_.Initials -eq "$MiddleInitial.")) } + if ($Match) { + Write-Verbose "$($MyInvocation.MyCommand) - Match(es) found!" + $Match + } else { + Write-Verbose "$($MyInvocation.MyCommand) - Match not found" + $false + } + } else { + Write-Verbose "$($MyInvocation.MyCommand) - Match not found. Either first name, middle initial or last name is null" + $false + } + } + + function Test-MatchFirstInitialLastName ($FirstName,$LastName) { + Write-Verbose -Message "$($MyInvocation.MyCommand) - Searching for AD user match based on first initial '$($FirstName.Substring(0,1))' and last name '$LastName'" + if ($FirstName -and $LastName) { + $Match = $AdUsers | where { "$($FirstName.SubString(0, 1))$LastName" -eq $_.samAccountName } + if ($Match) { + Write-Verbose "$($MyInvocation.MyCommand) - Match(es) found!" + $Match + } else { + Write-Verbose "$($MyInvocation.MyCommand) - Match not found" + $false + } + } else { + Write-Verbose "$($MyInvocation.MyCommand) - Match not found. Either first name or last name is null" + $false + } + } + <# + function Test-MatchLikeFirstNameLikeLastName ($FirstName, $LastName) { + if ($FirstName -and $LastName) { + Write-Verbose -Message "$($MyInvocation.MyCommand) - Searching for AD user match based on like first name '$FirstName', last name '$LastName'" + $Match = $AdUsers | where { ($FirstName -like "*$($_.givenName)*") -and ($LastName -like "*$($_.surName)*") } + if ($Match) { + Write-Verbose "$($MyInvocation.MyCommand) - Match(es) found!" + $Match + } else { + Write-Verbose "$($MyInvocation.MyCommand) - Match not found" + $false + } + } else { + Write-Verbose "$($MyInvocation.MyCommand) - Match not found. Either first or last name is null" + $false + } + } + #> + <# + function Test-MatchCommonFirstNameTranslationsLastName ($FirstName, $LastName) { + $Translations = @{ + 'Kathy' = 'Kathleen' + 'Randy' = 'Randall' + 'Bob' = 'Robert' + 'Rob' = 'Robert' + } + if ($FirstName -and $LastName) { + Write-Verbose -Message "$($MyInvocation.MyCommand) - Searching for AD user match based on first name '$FirstName', last name '$LastName'" + $Match = $AdUsers | where { ($FirstName -match $_.givenName) -and ($LastName -match $_.surName) } + if ($Match) { + Write-Verbose "$($MyInvocation.MyCommand) - Match(es) found!" + $Match + } else { + Write-Verbose "$($MyInvocation.MyCommand) - Match not found" + $false + } + } else { + Write-Verbose "$($MyInvocation.MyCommand) - Match not found. Either first or last name is null" + $false + } + } + #> + + #function Test-MatchLevenshteinDistance { + # + #} + #endregion + + #region ValidationTests + function Test-CsvField { + $CsvHeaders = (Get-Content $CsvFilePath | Select-Object -First 1).Split(',').Trim('"') + $AdToSourceFieldMappings.Values | foreach { + if (!($CsvHeaders -like $_)) { + return $false + } + } + $true + } + #endregion + + #region Functions + function Get-LevenshteinDistance { + # get-ld.ps1 (Levenshtein Distance) + # Levenshtein Distance is the # of edits it takes to get from 1 string to another + # This is one way of measuring the "similarity" of 2 strings + # Many useful purposes that can help in determining if 2 strings are similar possibly + # with different punctuation or misspellings/typos. + # + ######################################################## + + # Putting this as first non comment or empty line declares the parameters + # the script accepts + ########### + param ([string] $first, [string] $second, [switch] $ignoreCase) + + # No NULL check needed, why is that? + # PowerShell parameter handling converts Nulls into empty strings + # so we will never get a NULL string but we may get empty strings(length = 0) + ######################### + + $len1 = $first.length + $len2 = $second.length + + # If either string has length of zero, the # of edits/distance between them + # is simply the length of the other string + ####################################### + if ($len1 -eq 0) { return $len2 } + + if ($len2 -eq 0) { return $len1 } + + # make everything lowercase if ignoreCase flag is set + if ($ignoreCase -eq $true) { + $first = $first.tolowerinvariant() + $second = $second.tolowerinvariant() + } + + # create 2d Array to store the "distances" + $dist = new-object -type 'int[,]' -arg ($len1 + 1), ($len2 + 1) + + # initialize the first row and first column which represent the 2 + # strings we're comparing + for ($i = 0; $i -le $len1; $i++) { $dist[$i, 0] = $i } + for ($j = 0; $j -le $len2; $j++) { $dist[0, $j] = $j } + + $cost = 0 + + for ($i = 1; $i -le $len1; $i++) { + for ($j = 1; $j -le $len2; $j++) { + if ($second[$j - 1] -ceq $first[$i - 1]) { + $cost = 0 + } else { + $cost = 1 + } + + # The value going into the cell is the min of 3 possibilities: + # 1. The cell immediately above plus 1 + # 2. The cell immediately to the left plus 1 + # 3. The cell diagonally above and to the left plus the 'cost' + ############## + # I had to add lots of parentheses to "help" the Powershell parser + # And I separated out the tempmin variable for readability + $tempmin = [System.Math]::Min(([int]$dist[($i - 1), $j] + 1), ([int]$dist[$i, ($j - 1)] + 1)) + $dist[$i, $j] = [System.Math]::Min($tempmin, ([int]$dist[($i - 1), ($j - 1)] + $cost)) + } + } + + # the actual distance is stored in the bottom right cell + return $dist[$len1, $len2]; + } + + <# + function Test-DataRow ([object]$SourceRowData) { + ## Check for instances where all fields are null + $FieldCount = $SourceRowData.psObject.Properties.Name.Count + $NullValues = $SourceRowData.psObject.Properties.Value | where { $_ -eq $null } + if ($NullValues -and ($FieldCount -eq $NullValues.Count)) { + Write-Warning 'This source data row contains all null fields' + $false + } else { + $true + } + } + #> + + function New-OutputRow ([object]$SourceRowData) { + $OutputRow = [ordered]@{ + 'Match' = $false; + 'MatchTest' = 'N/A' + } + $AdToOutputFieldMappings.Values | foreach { + $OutputRow[$_] = 'N/A' + } + + $SourceRowData.psobject.Properties | foreach { + if ($_.Value) { + $OutputRow[$_.Name] = $_.Value + } + } + $OutputRow + } + + function Add-ToOutputRow ([hashtable]$OutputRow, [object]$AdRowData, $MatchTest) { + $AdToOutputFieldMappings.Keys | foreach { + if ($AdRowData.$_) { + $OutputRow[$AdToOutputFieldMappings[$_]] = $AdRowData.$_ + } + $OutputRow.MatchTest = $MatchTest + } + $OutputRow + } + + function Test-TestMatchValid ($FunctionParameters) { + $Compare = Compare-Object -ReferenceObject $FunctionParameters -DifferenceObject ($AdToSourceFieldMappings.Values | % { $_ }) -IncludeEqual -ExcludeDifferent + if (!$Compare) { + $false + } elseif ($Compare.Count -ne $FunctionParameters.Count) { + $false + } else { + $true + } + } + + function Get-FunctionParams ($Function) { + $Function.Parameters.Keys | where { $AdToSourceFieldMappings.Values -contains $_ } + } + #endregion + + ## Each row of the reference data source will be checked for a match with an AD user object by + ## attempting a match based on any of the Test-Match* functions. Each match function needs to be + ## assigned a priority. A match will be attempted starting from the highest priority match function + ## to the lowest until a match is made. Once the match is made, matching will stop. + $MatchFunctionPriorities = @{ + 'Test-MatchFirstNameMiddleInitialLastName' = 1 + 'Test-MatchFirstNameLastName' = 2 + #'Test-MatchLikeFirstNameLikeLastName' = 3 + 'Test-MatchFirstInitialLastName' = 4 + } + + if ($PSBoundParameters.CsvFilePath) { + Write-Verbose -Message "Verifying all field names in the $CsvFilePath match $($AdToSourceFieldMappings.Values -join ',')" + if (!(Test-CsvField)) { + throw "One or more fields specified in the `$AdToSourceFieldMappings param do not exist in the CSV file $CsvFilePath" + } else { + Write-Verbose "The CSV file's field match source field mappings" + } + } + + } catch { + Write-Error "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + return + } + + Write-Verbose -Message "Retrieving all Active Directory user objects..." + $script:AdUsers = Get-ADUser -Filter * -Properties 'DisplayName','Initials' +} + +process { + try { + ## Find all functions in memory that match Test-match*. This will automatically include all of our tests + ## without having to call them one at a time. This will also automatically match all functions with only + ## the fields the source data has + $TestFunctions = Get-ChildItem function:\Test-Match* | where { !$_.Module } + Write-Verbose "Found $($TestFunctions.Count) test functions in the script" + $MatchTestsToRun = @() + foreach ($TestFunction in $TestFunctions) { + Write-Verbose -Message "Checking to see if we'll use the $($TestFunction.Name) function" + if (Test-TestMatchValid -FunctionParameters ($TestFunction.Parameters.Keys | % { $_ })) { + Write-Verbose -Message "The source data has all of the function $($TestFunction.Name)'s parameters. We'll try this one" + $MatchTestsToRun += [System.Management.Automation.FunctionInfo]$TestFunction + } else { + Write-Verbose -Message "The parameters $($AdToSourceFieldMappings.Keys -join ',') are not adequate for the function $($TestFunction.Name)" + } + } + + ## Once all the tests have been gathered that apply to the fields in the data source match them with the + ## function name in the priorities hash table and sort them by priority + $MatchTestsToRun | foreach { + $Test = $_; + foreach ($i in $MatchFunctionPriorities.GetEnumerator()) { + if ($Test.Name -eq $i.Key) { + Write-Verbose "Assigning a priority of $($i.Value) to function $($Test.Name)" + $Test | Add-Member -NotePropertyName 'Priority' -NotePropertyValue $i.Value + } + } + } + $MatchTestsToRun = $MatchTestsToRun | Sort-Object Priority + + if ($CsvFilePath) { + $DataRows = Import-Csv -Path $CsvFilePath + } + + ## Add any future data sources here ensuring they all get assigned to $DataRows + + foreach ($Row in $DataRows) { + #if (Test-DataRow -SourceRowData $Row) { + [hashtable]$OutputRow = New-OutputRow -SourceRowData $Row + ## Run the match tests that only apply to the fields in the source data + ## This prevents tests from being run that have param requirements that our + ## source data doesn't have like when the source data only has first name and + ## last name. This means we couldn't run the Test-FirstNameMiddleInitialLastName function. + foreach ($Test in $MatchTestsToRun) { + Write-Verbose -Message "Running function $($Test.Name)..." + [array]$FuncParamKeys = Get-FunctionParams -Function $Test + [hashtable]$FuncParams = @{ } + [array]$FuncParamKeys | foreach { + $Row.psObject.Properties | foreach { + if ([array]$FuncParamKeys -contains [string]$_.Name) { + $FuncParams[$_.Name] = $_.Value + } + } + } + Write-Verbose -Message "Passing the parameters '$($FuncParams.Keys -join ',')' with values '$($FuncParams.Values -join ',')' to the function $($Test.Name)" + $AdTestResultObject = & $Test @FuncParams + if ($AdTestResultObject) { + $OutputRow.Match = $true + foreach ($i in $AdTestResultObject) { + $OutputRow = Add-ToOutputRow -AdRowData $i -OutputRow $OutputRow -MatchTest $Test.Name + } + break + } + } + #} + [pscustomobject]$OutputRow + } + } catch { + Write-Error "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + } +} \ No newline at end of file diff --git a/Get-CMDpContent.ps1 b/Get-CMDpContent.ps1 new file mode 100644 index 0000000..565fb49 --- /dev/null +++ b/Get-CMDpContent.ps1 @@ -0,0 +1,145 @@ +<# +.Synopsis + Gets the Objects you specify distributed to a Distribution Point +.DESCRIPTION + The Function queries the SMS_DistributionPoint to get a specific type of Objects being stored on a DistributionPoint. + Then it processes each Object to get the Name of the Application, package etc being stored on the DP. + Note for the Application Objects we use ModelName to filter instead of the PackageID as for SMS_Application WMI Class the property PackageID is lazy property +.EXAMPLE + get-DPContent -DPname dexsccm -ObjectType Application + + DP ObjectType Application + -- ---------- ----------- + dexsccm Application 7-Zip + dexsccm Application PowerShell Community Extensions + dexsccm Application {Quest Active Roles Managment Shell for AD, QuestAD} + dexsccm Application PowerShell Set PageFile + dexsccm Application {NotePad++, NotePad++, NotePad++, NotePad++...} + + Invoke the Function and ask only about the Applications distributed to the DP +.EXAMPLE + Get-DPContent -DPname dexsccm + + DP ObjectType Package PackageID + -- ---------- ------- --------- + dexsccm Package User State Migration Tool for Windows 8 DEX00001 + dexsccm Package Configuration Manager Client Package DEX00002 + + Invoke the Funtion and only ask for the Packages distributed to the DP +.LINK + http://dexterposh.blogspot.com/2014/07/powershell-sccm-2012-get-dpcontent.html +.NOTES + Author - DexterPOSH + Inspired by - Adam Bertram's tweet ;) +#> +function Get-DPContent { + [CmdletBinding()] + [OutputType([PSObject[]])] + Param + ( + # Specify the Distribution Point Name + [Parameter(Mandatory = $true, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [string[]]$DPname, + + #specfiy the ObjectType to get. Default is "Package" + [Parameter()] + [ValidateSet("Package", "Application", "ImagePackage", "BootImagePackage", "DriverPackage", "SoftwareUpdatePackage")] + [string]$ObjectType = "Package", + + #specify the SCCMServer having SMS Namespace provider installed for the site. Default is the local machine. + [Parameter(Mandatory = $false)] + [Alias("SMSProvider")] + [String]$SCCMServer = "dexsccm" + ) + + begin { + Write-Verbose -Message "[BEGIN] Starting Function" + #region open a CIM session + $CIMSessionParams = @{ + ComputerName = $SCCMServer + ErrorAction = 'Stop' + } + try { + if ((Test-WSMan -ComputerName $SCCMServer -ErrorAction SilentlyContinue).ProductVersion -match 'Stack: 3.0') { + Write-Verbose -Message "[BEGIN] WSMAN is responsive" + $CimSession = New-CimSession @CIMSessionParams + $CimProtocol = $CimSession.protocol + Write-Verbose -Message "[BEGIN] [$CimProtocol] CIM SESSION - Opened" + } else { + Write-Verbose -Message "[PROCESS] Attempting to connect with protocol: DCOM" + $CIMSessionParams.SessionOption = New-CimSessionOption -Protocol Dcom + $CimSession = New-CimSession @CIMSessionParams + $CimProtocol = $CimSession.protocol + + Write-Verbose -Message "[BEGIN] [$CimProtocol] CIM SESSION - Opened" + } + + #endregion open a CIM session + + $sccmProvider = Get-CimInstance -query "select * from SMS_ProviderLocation where ProviderForLocalSite = true" -Namespace "root\sms" -CimSession $CimSession -ErrorAction Stop + # Split up the namespace path + $Splits = $sccmProvider.NamespacePath -split "\\", 4 + Write-Verbose "[BEGIN] Provider is located on $($sccmProvider.Machine) in namespace $($splits[3])" + + # Create a new hash to be passed on later + $hash = @{ "CimSession" = $CimSession; "NameSpace" = $Splits[3]; "ErrorAction" = "Stop" } + + switch -exact ($ObjectType) { + 'Package' { $ObjectTypeID = 2; $ObjectClass = "SMS_Package"; break } + 'Application' { $ObjectTypeID = 31; $ObjectClass = "SMS_Application"; break } + 'ImagePackage' { $ObjectTypeID = 18; $ObjectClass = "SMS_ImagePackage"; break } + 'BootImagePackage' { $ObjectTypeID = 19; $ObjectClass = "SMS_BootImagePackage"; break } + 'DriverPackage' { $ObjectTypeID = 23; $ObjectClass = "SMS_DriverPackage"; break } + 'SoftwareUpdatePackage' { $ObjectTypeID = 24; $ObjectClass = "SMS_SoftwareUpdatesPackage"; break } + } + } catch { + Write-Warning "[BEGIN] $SCCMServer needs to have SMS Namespace Installed" + throw $Error[0].Exception + } + } + + process { + foreach ($DP in $DPname) { + Write-Verbose -Message "[PROCESS] Working with Distribution Point $DP" + try { + if ($ObjectType -eq "Application") { + #for the SMS_Application Objects the PackagedID is a Lazy property so have to filter on the ModelName property + $SecureObjectIDs = Get-CimInstance -query "Select SecureObjectID from SMS_DistributionPoint Where (ServerNALPath LIKE '%$DP%') AND (ObjectTypeID='$ObjectTypeID')" @hash | select -ExpandProperty SecureObjectId + + $SecureObjectIDs | foreach { + if ($App = Get-CimInstance -query "Select LocalizedDisplayName,LocalizedDescription from $ObjectClass WHERE ModelName='$_'" @hash | select -Unique) { + [pscustomobject]@{ + DP = $DP + ObjectType = $ObjectType + Application = $App.LocalizedDisplayName + Description = $app.LocalizedDescription + } + } + } + } else { + $PackageIDs = Get-CimInstance -query "Select SecureObjectID from SMS_DistributionPoint Where (ServerNALPath LIKE '%$DP%') AND (ObjectTypeID='$ObjectTypeID')" @hash | select -ExpandProperty SecureObjectID + + $PackageIDs | foreach { + if ($Package = Get-CimInstance -query "Select Name from $ObjectClass WHERE PackageID='$_'" @hash) { + [pscustomobject]@{ + DP = $DP + ObjectType = $ObjectType + Package = $Package.Name + PackageID = $_ + } + } + } + } + } catch { + Write-Warning "[PROCESS] Something went wrong while querying $SCCMServer for the DP or Object info" + throw $_.Exception + } + } + } + end { + Write-Verbose -Message "[END] Ending Function" + Remove-CimSession -CimSession $CimSession + } +} \ No newline at end of file diff --git a/Get-CmAppDeploymentStatus.ps1 b/Get-CmAppDeploymentStatus.ps1 new file mode 100644 index 0000000..ff36dfb --- /dev/null +++ b/Get-CmAppDeploymentStatus.ps1 @@ -0,0 +1,128 @@ +<# +.SYNOPSIS + This script checks status of a deployed application to members of a SCCM collection or a single SCCM client +.NOTES + Requires the SQL PSCX modules here https://sqlpsx.codeplex.com/documentation +.EXAMPLE + PS> Get-CmAppDeploymentStatus.ps1 -CollectionName 'My Collection' -ApplicationName MyApplication + + This example enumerates all collection members in the collection 'My Collection' then evaluates each of them + to see what the status of the application MyApplication is. +.PARAMETER CollectionName + The name of the SCCM collection you'd like to query members in +.PARAMETER Computername + The name of one or more PCs to check application deployment status +.PARAMETER ApplicationName + The name of the application to check the status of +.PARAMETER SiteServer + Your SCCM site server +.PARAMETER SiteCode + The 3 character SCCM site code +#> +[CmdletBinding(DefaultParameterSetName = 'None')] +param ( + [Parameter(ParameterSetName = 'Collection', Mandatory)] + [string]$CollectionName, + [Parameter(ParameterSetName = 'Computer', Mandatory)] + [string[]]$Computername, + [string]$ApplicationName, + [string]$SiteServer = 'MYSITESERVER', + [string]$SiteCode = 'CON' +) + +begin { + Set-StrictMode -Version Latest + + function Get-CmCollectionMember ($Collection) { + try { + $Ns = "ROOT\sms\site_$SiteCode" + $Col = Get-CimInstance -ComputerName $SiteServer -Class 'SMS_Collection' -Namespace $Ns -Filter "Name = '$Collection'" + $ColId = $Col.CollectionID; + Get-CimInstance -Computername $SiteServer -Namespace $Ns -Class "SMS_CM_RES_COLL_$ColId" + } catch { + Write-Error $_.Exception.Message + } + } + + function Get-CmClientAppDeploymentStatus ($Computername,$ApplicationName) { + $EvalStates = @{ + 0 = 'No state information is available'; + 1 = 'Application is enforced to desired/resolved state'; + 2 = 'Application is not required on the client'; + 3 = 'Application is available for enforcement (install or uninstall based on resolved state). Content may/may not have been downloaded'; + 4 = 'Application last failed to enforce (install/uninstall)'; + 5 = 'Application is currently waiting for content download to complete'; + 6 = 'Application is currently waiting for content download to complete'; + 7 = 'Application is currently waiting for its dependencies to download'; + 8 = 'Application is currently waiting for a service (maintenance) window'; + 9 = 'Application is currently waiting for a previously pending reboot'; + 10 = 'Application is currently waiting for serialized enforcement'; + 11 = 'Application is currently enforcing dependencies'; + 12 = 'Application is currently enforcing'; + 13 = 'Application install/uninstall enforced and soft reboot is pending'; + 14 = 'Application installed/uninstalled and hard reboot is pending'; + 15 = 'Update is available but pending installation'; + 16 = 'Application failed to evaluate'; + 17 = 'Application is currently waiting for an active user session to enforce'; + 18 = 'Application is currently waiting for all users to logoff'; + 19 = 'Application is currently waiting for a user logon'; + 20 = 'Application in progress, waiting for retry'; + 21 = 'Application is waiting for presentation mode to be switched off'; + 22 = 'Application is pre-downloading content (downloading outside of install job)'; + 23 = 'Application is pre-downloading dependent content (downloading outside of install job)'; + 24 = 'Application download failed (downloading during install job)'; + 25 = 'Application pre-downloading failed (downloading outside of install job)'; + 26 = 'Download success (downloading during install job)'; + 27 = 'Post-enforce evaluation'; + 28 = 'Waiting for network connectivity'; + } + + $Params = @{ + 'Computername' = $Computername + 'Namespace' = 'root\ccm\clientsdk' + 'Class' = 'CCM_Application' + } + if ($ApplicationName) { + Get-WmiObject @Params | Where-Object { $_.FullName -eq $ApplicationName } | Select-Object PSComputerName, Name, InstallState, ErrorCode, @{ n = 'EvalState'; e = { $EvalStates[[int]$_.EvaluationState] } }, @{ label = 'ApplicationMadeAvailable'; expression = { $_.ConvertToDateTime($_.StartTime) } } + } else { + Get-WmiObject @Params | Select-Object PSComputerName, Name, InstallState, ErrorCode, @{ n = 'EvalState'; e = { $EvalStates[[int]$_.EvaluationState] } }, @{ label = 'ApplicationMadeAvailable'; expression = { $_.ConvertToDateTime($_.StartTime) } } + } + } + + function Test-Ping ($ComputerName) { + try { + $oPing = new-object system.net.networkinformation.ping; + if (($oPing.Send($ComputerName, 200).Status -eq 'TimedOut')) { + $false + } else { + $true + } + } catch [System.Exception] { + $false + } + } +} + +process { + if ($CollectionName) { + $Clients = (Get-CmCollectionMember -Collection $CollectionName).Name + } else { + $Clients = $Computername + } + Write-Verbose "Will query '$($Clients.Count)' clients" + foreach ($Client in $Clients) { + try { + if (!(Test-Ping -ComputerName $Client)) { + throw "$Client is offline" + } else { + $Params = @{ 'Computername' = $Client } + if ($ApplicationName) { + $Params.ApplicationName = $ApplicationName + } + Get-CmClientAppDeploymentStatus @Params + } + } catch { + Write-Warning $_.Exception.Message + } + } +} \ No newline at end of file diff --git a/Get-DhcpLeasesInDomain.ps1 b/Get-DhcpLeasesInDomain.ps1 new file mode 100644 index 0000000..7a32480 --- /dev/null +++ b/Get-DhcpLeasesInDomain.ps1 @@ -0,0 +1,40 @@ +#Requires -Module DhcpServer + +<# +.SYNOPSIS + This script finds all DHCP servers in Active Directory, all scopes on all DHCP servers + and outputs all leases in all of those scopes +.NOTES + Created on: 12/10/2014 + Created by: Adam Bertram + Filename: Get-DhcpLeasesInDomain.ps1 +.EXAMPLE + PS> .\Get-DhcpLeasesInDomain.ps1 + +#> +[CmdletBinding()] +[OutputType('Selected.Microsoft.Management.Infrastructure.CimInstance')] +param () + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest +} + +process { + try { + $DhcpServers = Get-DhcpServerInDC | where { Test-Connection -ComputerName $_.DnsName -Quiet -Count 1 } | Select-Object -ExpandProperty DnsName + foreach ($DhcpServer in $DhcpServers) { + try { + try { $Scopes = Get-DhcpServerv4Scope -ComputerName $DhcpServer } catch {Write-Warning -Message "Error: $($_.Exception.Message)"} + foreach ($Scope in $Scopes) { + try { Get-DhcpServerv4Lease -ComputerName $DhcpServer -ScopeId $Scope.ScopeId | Select-Object * -ExcludeProperty 'Cim*' } catch { Write-Warning -Message "Error: $($_.Exception.Message)" } + } + } catch { + Write-Error -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + } + } +} catch { + Write-Error -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" +} +} \ No newline at end of file diff --git a/Get-DynamicDNSRecordsToBeScavenged.ps1 b/Get-DynamicDNSRecordsToBeScavenged.ps1 new file mode 100644 index 0000000..3ed0f54 --- /dev/null +++ b/Get-DynamicDNSRecordsToBeScavenged.ps1 @@ -0,0 +1,111 @@ +#Requires -Module ActiveDirectory,DnsServer + +<# +.SYNOPSIS + This script will report on all dynamic DNS records in a particular DNS zone that + are at risk of being scavenged by the DNS scavenging process. +.NOTES + Created on: 8/22/2014 + Created by: Adam Bertram + Filename: Get-RecordsToBeScavenged.ps1 + Credits: + Requirements: An AD-integrated DNS zone +.EXAMPLE + PS> Get-RecordsToBeScavenged.ps1 -DnsZone myzone -WarningDays 5 + + This example will find all DNS records in the zone 'myzone' that are set to be scavenged + within 5 days. +.PARAMETER DnsServer + The DNS server that will be queried +.PARAMETER DnsZone + The DNS zone that will be used to find records +.PARAMETER WarningDays + The number of days ahead of scavenge time you'd like to report on. By default, this script + only displays DNS records set to be scavenged within 1 day. +#> +[CmdletBinding()] +[OutputType('System.Management.Automation.PSCustomObject')] +param ( + [Parameter(Mandatory)] + [string]$DnsZone, + [Parameter()] + [string]$DnsServer = (Get-ADDomain).ReplicaDirectoryServers[0], + [Parameter()] + [int]$WarningDays = 1 +) +begin { + function Get-DnsHostname ($IPAddress) { + ## Use nslookup because it's much faster than any other cmdlet + $Result = nslookup $IPAddress 2> $null + $Result| where { $_ -match 'name' } | foreach { + $_.Replace('Name: ', '') + } + } + Function Test-Ping ($ComputerName) { + try { + $oPing = new-object system.net.networkinformation.ping; + if (($oPing.Send($ComputerName, 200).Status -eq 'TimedOut')) { + $false; + } else { + $true + } + } catch [System.Exception] { + $false + } + } +} +process { + try { + ## Check if scavenging and aging is even enabled on the server and zone + $ServerScavenging = Get-DnsServerScavenging -Computername $DnsServer + $ZoneAging = Get-DnsServerZoneAging -Name $DnsZone -ComputerName $DnsServer + if (!$ServerScavenging.ScavengingState) { + Write-Warning "Scavenging not enabled on server '$DnsServer'" + $NextScavengeTime = 'N/A' + } else { + $NextScavengeTime = $ServerScavenging.LastScavengeTime + $ServerScavenging.ScavengingInterval + } + if (!$ZoneAging.AgingEnabled) { + Write-Warning "Aging not enabled on zone '$DnsZone'" + } + + ## A record won't be scavengable until the refresh + no-refresh period has elapsed. Set a threshold + ## of this time plus a buffer to give the user a heads up ahead of time. + $StaleThreshold = ($ZoneAging.NoRefreshInterval.Days + $ZoneAging.RefreshInterval.Days) + $WarningDays + + ## Find all dynamic DNS host records in the zone that haven't updated their timestamp in a long time + ## ensuring to only include the hosts ending with the zone name. If not, by default, Get-DnsServerResourceRecord + ## will include a record with and without the zone name appended + $StaleRecords = Get-DnsServerResourceRecord -ComputerName $DnsServer -ZoneName $DnsZone -RRType A | where { $_.TimeStamp -and ($_.Timestamp -le (Get-Date).AddDays("-$StaleThreshold")) -and ($_.Hostname -like "*.$DnsZone") } + foreach ($StaleRecord in $StaleREcords) { + ## Get the IP address of the host to preform a reverse DNS lookup later + $RecordIp = $StaleRecord.RecordData.IPV4Address.IPAddressToString + ## Perform a reverse DNS lookup to find the actual hostname for that IP address. + ## Sometimes when a record has been out of commission for a long time duplicate + ## records can be created and the actual hostname for the IP address doesn't match + ## the old DNS record hostname anymore. + $ActualHostname = Get-DnsHostname $RecordIp + if ($ActualHostname) { + ## There's a PTR record for the host record. Ping the hostname to see if it's + ## still online. This is to only pay attention to the computers that may still + ## be online but have a problem updating their record. + $HostOnline = Test-Ping -Computername $ActualHostname + } else { + $HostOnline = 'N/A' + } + [pscustomobject]@{ + 'Server' = $DnsServer + 'Zone' = $DnsZone + 'RecordHostname' = $StaleRecord.Hostname + 'RecordTimestamp' = $StaleRecord.Timestamp + 'IsScavengable' = (@{ $true = $false; $false = $true }[$NextScavengeTime -eq 'N/A']) + 'ToBeScavengedOn' = $NextScavengeTime + 'ValidHostname' = $ActualHostname + 'RecordMatchesValidHostname' = $ActualHostname -eq $StaleRecord.Hostname + 'HostOnline' = (@{ $true = $HostOnline; $false = 'N/A' }[$ActualHostname -eq $StaleRecord.Hostname]) + } + } + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Get-FreeHardDriveSpace.ps1 b/Get-FreeHardDriveSpace.ps1 new file mode 100644 index 0000000..b216b3c --- /dev/null +++ b/Get-FreeHardDriveSpace.ps1 @@ -0,0 +1,74 @@ +<# +.SYNOPSIS + This finds the total hard drive free space for one or multiple hard drive partitions +.DESCRIPTION + This finds the total hard drive free space for one or multiple hard drive partitions. It returns free space + rounded to the nearest SizeOutputLabel parameter +.PARAMETER DriveLetter + This is the drive letter of the hard drive partition you'd like to query. By default, all drive letters are queried. +.PARAMETER SizeOutputLabel + In what size increments you'd like the size returned (KB, MB, GB, TB). Defaults to MB. +.PARAMETER Computername + The computername(s) you'd like to find free space on. This defaults to the local machine. +.EXAMPLE + PS C:\> Get-DriveFreeSpace -DriveLetter 'C','D' + This example retrieves the free space on the C and D drive partition. +#> +[CmdletBinding()] +[OutputType([array])] +param +( + [string[]]$Computername = 'localhost', + [Parameter(ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidatePattern('[A-Z]')] + [string]$DriveLetter, + [ValidateSet('KB','MB','GB','TB')] + [string]$SizeOutputLabel = 'MB' + +) + +Begin { + try { + $WhereQuery = "SELECT FreeSpace,DeviceID FROM Win32_Logicaldisk" + + if ($PsBoundParameters.DriveLetter) { + $WhereQuery += ' WHERE' + $BuiltQueryParams = { @() }.Invoke() + foreach ($Letter in $DriveLetter) { + $BuiltQueryParams.Add("DeviceId = '$DriveLetter`:'") + } + $WhereQuery = "$WhereQuery $($BuiltQueryParams -join ' OR ')" + } + Write-Debug "Using WQL query $WhereQuery" + $WmiParams = @{ + 'Query' = $WhereQuery + 'ErrorVariable' = 'MyError'; + 'ErrorAction' = 'SilentlyContinue' + } + } catch { + Write-Error $_.Exception.Message + } +} +Process { + try { + foreach ($Computer in $Computername) { + $WmiParams.Computername = $Computer + $WmiResult = Get-WmiObject @WmiParams + if ($MyError) { + throw $MyError + } + foreach ($Result in $WmiResult) { + if ($Result.Freespace) { + [pscustomobject]@{ + 'Computername' = $Computer; + 'DriveLetter' = $Result.DeviceID; + 'Freespace' = [int]($Result.FreeSpace / "1$SizeOutputLabel") + } + } + } + } + } catch { + Write-Error $_.Exception.Message + } + \ No newline at end of file diff --git a/Get-LocalGroupMembership.ps1 b/Get-LocalGroupMembership.ps1 new file mode 100644 index 0000000..4cd4476 --- /dev/null +++ b/Get-LocalGroupMembership.ps1 @@ -0,0 +1,5 @@ +param($Computername,$Group) +$group = [ADSI]"WinNT://$Computername/$Group" +@($group.Invoke("Members")) | + +foreach { $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null) } \ No newline at end of file diff --git a/Get-LocalPort.ps1 b/Get-LocalPort.ps1 new file mode 100644 index 0000000..4b6e7f2 --- /dev/null +++ b/Get-LocalPort.ps1 @@ -0,0 +1,111 @@ +#Requires -Version 3 +<# +.SYNOPSIS + This parses the native netstat.exe's output using the command line "netstat -anb" to find + all of the network ports in use on a local machine and all associated processes and services +.NOTES + Created on: 2/15/2015 + Created by: Adam Bertram + Filename: Get-LocalPort.ps1 +.EXAMPLE + PS> Get-LocalPort.ps1 + + This example will find all network ports in uses on the local computer with associated + processes and services + +.EXAMPLE + PS> Get-LocalPort.ps1 | Where-Object {$_.ProcessOwner -eq 'svchost.exe'} + + This example will find all network ports in use on the local computer that were opened + by the svchost.exe process. + +.EXAMPLE + PS> Get-LocalPort.ps1 | Where-Object {$_.IPVersion -eq 'IPv4'} + + This example will find all network ports in use on the local computer using IPv4 only. +#> +[CmdletBinding()] +param () + +begin { + Set-StrictMode -Version Latest + $ErrorActionPreference = 'Stop' +} + +process { + try { + ## Capture the output of the native netstat.exe utility + ## Remove the top row from the result and trim off any leading or trailing spaces from each line + ## Replace all instances of more than 1 space with a pipe symbol. This allows easier parsing of + ## the fields + $Netstat = (netstat -anb | where {$_ -and ($_ -ne 'Active Connections')}).Trim() | Select-Object -Skip 1 | foreach {$_ -replace '\s{2,}','|'} + + $i = 0 + foreach ($Line in $Netstat) { + ## Create the hashtable to conver to object later + $Out = @{ + 'Protocol' = '' + 'State' = '' + 'IPVersion' = '' + 'LocalAddress' = '' + 'LocalPort' = '' + 'RemoteAddress' = '' + 'RemotePort' = '' + 'ProcessOwner' = '' + 'Service' = '' + } + ## If the line is a port + if ($Line -cmatch '^[A-Z]{3}\|') { + $Cols = $Line.Split('|') + $Out.Protocol = $Cols[0] + ## Some ports don't have a state. If they do, there's always 4 fields in the line + if ($Cols.Count -eq 4) { + $Out.State = $Cols[3] + } + ## All port lines that start with a [ are IPv6 + if ($Cols[1].StartsWith('[')) { + $Out.IPVersion = 'IPv6' + $Out.LocalAddress = $Cols[1].Split(']')[0].TrimStart('[') + $Out.LocalPort = $Cols[1].Split(']')[1].TrimStart(':') + if ($Cols[2] -eq '*:*') { + $Out.RemoteAddress = '*' + $Out.RemotePort = '*' + } else { + $Out.RemoteAddress = $Cols[2].Split(']')[0].TrimStart('[') + $Out.RemotePort = $Cols[2].Split(']')[1].TrimStart(':') + } + } else { + $Out.IPVersion = 'IPv4' + $Out.LocalAddress = $Cols[1].Split(':')[0] + $Out.LocalPort = $Cols[1].Split(':')[1] + $Out.RemoteAddress = $Cols[2].Split(':')[0] + $Out.RemotePort = $Cols[2].Split(':')[1] + } + ## Because the process owner and service are on separate lines than the port line and the number of lines between them is variable + ## this craziness was necessary. This line starts parsing the netstat output at the current port line and searches for all + ## lines after that that are NOT a port line and finds the first one. This is how many lines there are until the next port + ## is defined. + $LinesUntilNextPortNum = ($Netstat | Select-Object -Skip $i | Select-String -Pattern '^[A-Z]{3}\|' -NotMatch | Select-Object -First 1).LineNumber + ## Add the current line to the number of lines until the next port definition to find the associated process owner and service name + $NextPortLineNum = $i + $LinesUntilNextPortNum + ## This would contain the process owner and service name + $PortAttribs = $Netstat[($i+1)..$NextPortLineNum] + ## The process owner is always enclosed in brackets of, if it can't find the owner, starts with 'Can' + $Out.ProcessOwner = $PortAttribs -match '^\[.*\.exe\]|Can' + if ($Out.ProcessOwner) { + ## Get rid of the brackets and pick the first index because this is an array + $Out.ProcessOwner = ($Out.ProcessOwner -replace '\[|\]','')[0] + } + ## A service is always a combination of multiple word characters at the start of the line + if ($PortAttribs -match '^\w+$') { + $Out.Service = ($PortAttribs -match '^\w+$')[0] + } + [pscustomobject]$Out + } + ## Keep the counter + $i++ + } + } catch { + Write-Error "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + } +} \ No newline at end of file diff --git a/Get-MoreCowbell.ps1 b/Get-MoreCowbell.ps1 new file mode 100644 index 0000000..58b6802 --- /dev/null +++ b/Get-MoreCowbell.ps1 @@ -0,0 +1,58 @@ +function Get-MoreCowbell +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [switch]$Introduction, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [int]$Repeat = 10, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string]$CowbellUrl = 'http://emmanuelprot.free.fr/Drums%20kit%20Manu/Cowbell.wav', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string]$IntroUrl = 'http://www.innervation.com/crap/cowbell.wav' + + + ) + begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + } + process { + try + { + $sound = new-Object System.Media.SoundPlayer + $CowBellLoc = "$($env:TEMP)\Cowbell.wav" + if (-not (Test-Path -Path $CowBellLoc -PathType Leaf)) + { + Invoke-WebRequest -Uri $CowbellUrl -OutFile $CowBellLoc + } + if ($Introduction.IsPresent) + { + $IntroLoc = "$($env:TEMP)\CowbellIntro.wav" + if (-not (Test-Path -Path $IntroLoc -PathType Leaf)) + { + Invoke-WebRequest -Uri $IntroUrl -OutFile $IntroLoc + } + $sound.SoundLocation = $IntroLoc + $sound.Play() + sleep 2 + } + $sound.SoundLocation = $CowBellLoc + for ($i=0; $i -lt $Repeat; $i++) { + $sound.Play(); + Start-Sleep -Milliseconds 500 + } + } + catch + { + Write-Error $_.Exception.Message + } + } +} \ No newline at end of file diff --git a/Get-RecordsToBeScavenged.ps1 b/Get-RecordsToBeScavenged.ps1 new file mode 100644 index 0000000..61299b0 --- /dev/null +++ b/Get-RecordsToBeScavenged.ps1 @@ -0,0 +1,178 @@ +#Requires -Version 3 +#Requires -Module ActiveDirectory,DnsServer + +<# +.SYNOPSIS + This script will report on all dynamic DNS records in a particular DNS zone that + are at risk of being scavenged by the DNS scavenging process. +.NOTES + Created on: 8/22/2014 + Created by: Adam Bertram + Filename: Get-RecordsToBeScavenged.ps1 + Credits: + Requirements: An AD-integrated DNS zone +.EXAMPLE + PS> Get-RecordsToBeScavenged.ps1 -DnsZone myzone -WarningDays 5 + + This example will find all DNS records in the zone 'myzone' that are set to be scavenged + within 5 days. +.PARAMETER DnsServer + The DNS server that will be queried +.PARAMETER DnsZone + The DNS zone that will be used to find records +.PARAMETER CheckValidity + Use this switch parameter to verify each record that's showing as "stale". This attempts to check + a few criteria like ensuring the stale record actually represents the hostname and if that host + is currently online. +.PARAMETER WarningDays + The number of days ahead of scavenge time you'd like to report on. By default, this script + only displays DNS records set to be scavenged within 1 day. +.PARAMETER EmailAddress + An email address that you'd like to send the list of stale DNS records to. +.PARAMETER NetbiosDomainName + The single-label of the Active Directory domain you're in +.PARAMETER NbtScanFilePath + The file path of the nbtscan.exe utility +#> +[CmdletBinding()] +[OutputType('System.Management.Automation.PSCustomObject')] +param ( + [Parameter(Mandatory)] + [string]$DnsZone, + [Parameter()] + [switch]$CheckValidity, + [Parameter()] + [string]$DnsServer = (Get-ADDomain).ReplicaDirectoryServers[0], + [Parameter()] + [int]$WarningDays = 1, + [string]$EmailAddress, + [Parameter()] + [string]$NetbiosDomainName = (Get-AdDomain).NetBIOSName, + [Parameter()] + [ValidateScript({ Test-Path -Path $_ -PathType Leaf })] + [string]$NbtScanFilePath = 'C:\nbtscan.exe' +) +begin { + function Get-DnsHostname ($IPAddress) { + try { + ## Use nslookup because it's much faster than any other cmdlet + $Result = nslookup $IPAddress 2> $null + $Result | where { $_ -match 'name' } | foreach { + $_.Replace('Name: ', '') + } + } catch { + Write-Warning "Could not find DNS hostname for IP $IpAddress" + $false + } + } + Function Test-Ping ($ComputerName) { + $Result = ping $Computername -n 2 + if ($Result | where { $_ -match 'Reply from ' }) { + $true + } else { + $false + } + } + + function Get-Computername ($IpAddress) { + try { + & $NbtScanFilePath $IpAddress 2> $null | where { $_ -match "$NetbiosDomainName\\(.*) " } | foreach { $matches[1].Trim() } + } catch { + Write-Warning -Message "Failed to get computer name for IP $IpAddress" + $false + } + } +} +process { + try { + ## Check if scavenging and aging is even enabled on the server and zone + $ServerScavenging = Get-DnsServerScavenging -Computername $DnsServer + $ZoneAging = Get-DnsServerZoneAging -Name $DnsZone -ComputerName $DnsServer + if (!$ServerScavenging.ScavengingState) { + Write-Warning "Scavenging not enabled on server '$DnsServer'" + $NextScavengeTime = 'N/A' + } else { + $NextScavengeTime = $ServerScavenging.LastScavengeTime + $ServerScavenging.ScavengingInterval + Write-Verbose "The next scavenge time is '$NextScavengeTime'" + } + if (!$ZoneAging.AgingEnabled) { + Write-Warning "Aging not enabled on zone '$DnsZone'" + $NextScavengeTime = 'N/A' + } + + ## A record won't be scavengable until the refresh + no-refresh period has elapsed. Set a threshold + ## of this time plus a buffer to give the user a heads up ahead of time. + $StaleThreshold = ($ZoneAging.NoRefreshInterval.Days + $ZoneAging.RefreshInterval.Days) + $WarningDays + Write-Verbose "The stale threshold is '$StaleThreshold' days" + + ## Find all dynamic DNS host records in the zone that haven't updated their timestamp in a long time + ## ensuring to only include the hosts ending with the zone name. If not, by default, Get-DnsServerResourceRecord + ## will include a record with and without the zone name appended + $StaleRecords = Get-DnsServerResourceRecord -ComputerName $DnsServer -ZoneName $DnsZone -RRType A | where { $_.TimeStamp -and ($_.Timestamp -le (Get-Date).AddDays("-$StaleThreshold")) -and ($_.Hostname -like "*.$DnsZone") } + Write-Verbose "Found '$($StaleRecords.Count)' stale records in zone '$DnsZone' on server '$DnsServer'" + $EmailRecords = @() + foreach ($StaleRecord in $StaleRecords) { + $Output = @{ + 'Server' = $DnsServer + 'Zone' = $DnsZone + 'RecordHostname' = $StaleRecord.Hostname + 'RecordTimestamp' = $StaleRecord.Timestamp + 'ReversePTRRecord' = 'N/A' + 'ReverseNetBIOSHostname' = 'N/A' + 'IsScavengable' = (@{ $true = $false; $false = $true }[$NextScavengeTime -eq 'N/A']) + 'ToBeScavengedOn' = $NextScavengeTime + 'RecordMatchesNetBiosHostname' = 'N/A' + 'HostOnline' = 'N/A' + } + if ($CheckValidity.IsPresent) { + Write-Verbose "Checking stale record '$($StaleRecord.Hostname)'" + ## Get the IP address of the host to preform a reverse DNS lookup later + $RecordIp = $StaleRecord.RecordData.IPV4Address.IPAddressToString + $ReverseDnsHostname = Get-DnsHostname -IPAddress $RecordIp + if ($ReverseDnsHostname) { + Write-Verbose "DNS PTR record is '$ReverseDnsHostname'" + $Output.ReversePTRRecord = $ReverseDnsHostname + } else { + Write-Verbose "No DNS PTR record exists for record '$($StaleRecord.Hostname)'" + } + $ReverseNetbiosHostname = Get-Computername -IpAddress $RecordIp + if ($ReverseNetbiosHostname) { + Write-Verbose "Netbios hostname is '$ReverseNetbiosHostname'" + $Output.ReverseNetBIOSHostname = $ReverseNetbiosHostname + } else { + Write-Verbose "No Netbios hostname exists for record '$($StaleRecord.Hostname)'" + } + + if ($ReverseNetbiosHostname -eq $StaleRecord.Hostname.Replace(".$DnsZone", '')) { + Write-Verbose "Netbios hostname matches DNS record hostname" + $Output.RecordMatchesNetBiosHostname = $true + $Output.HostOnline = Test-Ping -Computername $ReverseNetbiosHostname + } + } + if ($EmailAddress) { + $EmailRecords += [pscustomobject]$Output + } else { + [pscustomobject]$Output + } + } + if ($EmailAddress) { + $EmailRecords = @([pscustomobject]@{ 'test'='dfdfdfd'}, [pscustomobject]@{ 'teddddst' = 'dfdfdddddfd' }) + if ($EmailRecords.Count -eq 0) { + Write-Verbose "No stale records found to email" + } else { + Write-Verbose "Emailing the list of $($EmailRecords.Count) stale records to $EmailAddress" + $Params = @{ + 'From' = 'Union Hospital '; + 'To' = $EmailAddress; + 'Subject' = 'UNH DNS Records To be Scavenged'; + 'SmtpServer' = 'smtp.uhhg.org' + 'Body' = $EmailRecords | Out-String + } + + Send-MailMessage @Params + } + } + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Get-ServiceListeningPort.ps1 b/Get-ServiceListeningPort.ps1 new file mode 100644 index 0000000..eb891c0 --- /dev/null +++ b/Get-ServiceListeningPort.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS + +.NOTES + Created on: 8/22/2014 + Created by: Adam Bertram + Filename: + Credits: + Requirements: + Todos: +.EXAMPLE + +.EXAMPLE + +.PARAMETER PARAM1 + +.PARAMETER PARAM2 + +#> +[CmdletBinding(DefaultParameterSetName = 'name')] +[OutputType('System.Management.Automation.PSCustomObject')] +param ( + [Parameter(ParameterSetName = 'name', + Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [ValidateSet("Tom","Dick","Jane")] + [ValidateRange(21,65)] + [ValidateScript({Test-Path $_ -PathType 'Container'})] + [ValidateNotNullOrEmpty()] + [ValidateCount(1,5)] + [ValidateLength(1,10)] + [ValidatePattern()] + [string]$Computername = 'localhost' +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest +} + +process { + try { + $ServiceExes = Get-WmiObject -ComputerName $Computername -Property DisplayName, PathName + + } catch { + Write-Error $_.Exception.Message + } +} + +end { + try { + + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Get-UpTime.ps1 b/Get-UpTime.ps1 new file mode 100644 index 0000000..2fdfb9e --- /dev/null +++ b/Get-UpTime.ps1 @@ -0,0 +1,43 @@ +<# +.SYNOPSIS + This script gets the total time a computer has been up for. +.DESCRIPTION + This script queries the Win32_OperatingSystem WMI class to retrieve the + LastBootupTime and output a datetime object containing the specific + time the computer has been up for. +.NOTES + Created on: 6/9/2014 + Created by: Adam Bertram + Filename: Get-UpTime.ps1 +.EXAMPLE + .\Get-Uptime.ps1 -Computername 'COMPUTER' + +.PARAMETER Computername + This is the name of the computer you'd like to query +#> +[CmdletBinding()] +param ( + [Parameter(ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [string]$Computername = 'localhost' +) + +begin { + Set-StrictMode -Version Latest +} + +process { + try { + $WmiResult = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName + $LastBoot = $WmiResult.ConvertToDateTime($WmiResult.LastBootupTime) + $ObjParams = [ordered]@{'Computername' = $Computername } + ((Get-Date) - $LastBoot).psobject.properties | foreach { $ObjParams[$_.Name] = $_.Value } + New-Object -TypeName PSObject -Property $ObjParams + } catch { + Write-Error $_.Exception.Message + } +} + +end { + +} \ No newline at end of file diff --git a/Import-Certificate.ps1 b/Import-Certificate.ps1 new file mode 100644 index 0000000..c07d0eb --- /dev/null +++ b/Import-Certificate.ps1 @@ -0,0 +1,69 @@ +<# +.SYNOPSIS + This script imports a certificate into any certificate store on a local computer +.NOTES + Created on: 8/6/2014 + Created by: Adam Bertram + Filename: Import-Certificate.ps1 +.EXAMPLE + PS> .\Import-Certificate.ps1 -Location LocalMachine -StoreName My -FilePath C:\certificate.cer + + This example will import the certificate.cert certificate into the Personal store for the + local computer +.EXAMPLE + PS> .\Import-Certificate.ps1 -Location CurrentUser -StoreName TrustedPublisher -FilePath C:\certificate.cer + + This example will import the certificate.cer certificate into the Trusted Publishers store for the + currently logged on user +.PARAMETER Location + This is the location (either CurrentUser or LocalMachine) where the store is located which the certificate + will go into +.PARAMETER StoreName + This is the certificate store that the certificate will be placed into +.PARAMETER FilePath + This is the path to the certificate file that you'd like to import +#> +[CmdletBinding()] +[OutputType()] +param ( + [Parameter(Mandatory)] + [ValidateSet('CurrentUser', 'LocalMachine')] + [string]$Location, + [Parameter(Mandatory)] + [ValidateScript({ + if ($Location -eq 'CurrentUser') { + (Get-ChildItem Cert:\CurrentUser | select -ExpandProperty name) -contains $_ + } else { + (Get-ChildItem Cert:\LocalMachine | select -ExpandProperty name) -contains $_ + } + })] + [string]$StoreName, + [Parameter(Mandatory)] + [ValidateScript({Test-Path $_ -PathType Leaf })] + [string]$FilePath +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + try { + [void][System.Reflection.Assembly]::LoadWithPartialName("System.Security") + } catch { + Write-Error $_.Exception.Message + } +} + +process { + try { + $Cert = Get-Item $FilePath + $Cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $Cert + foreach ($Store in $StoreName) { + $X509Store = New-Object System.Security.Cryptography.X509Certificates.X509Store $Store, $Location + $X509Store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + $X509Store.Add($Cert) + $X509Store.Close() + } + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Invoke-RemoteScript.ps1 b/Invoke-RemoteScript.ps1 new file mode 100644 index 0000000..efa4c39 --- /dev/null +++ b/Invoke-RemoteScript.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS + +.NOTES + Created on: 5/9/2014 1:48 PM + Created by: Adam Bertram + Organization: + Filename: +.DESCRIPTION + +.EXAMPLE + +.EXAMPLE + +.PARAMETER FolderPath + Any folders (on the local computer) that need copied to the remote computer prior to execution +.PARAMETER ScriptPath + The Powershell script path (on the local computer) that needs executed on the remote computer +.PARAMETER RemoteDrive + The remote drive letter the script will be executed on and the folder will be copied to +.PARAMETER Computername + The remote computer to execute files on. +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory = $True, + ValueFromPipeline = $True, + ValueFromPipelineByPropertyName = $True)] + [string]$Computername, + [Parameter(Mandatory = $True, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $False)] + [string]$FolderPath, + [Parameter(Mandatory = $True, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $False)] + [string]$ScriptPath, + [Parameter(Mandatory = $False, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $False)] + [string]$RemoteDrive = 'C' +) + +begin { + ## http://www.leeholmes.com/blog/2009/11/20/testing-for-powershell-remoting-test-psremoting/ + function Test-PsRemoting { + param ( + [Parameter(Mandatory = $true)] + $computername + ) + + try { + $errorActionPreference = "Stop" + $result = Invoke-Command -ComputerName $computername { 1 } + } catch { + Write-Verbose $_ + return $false + } + + ## I’ve never seen this happen, but if you want to be + ## thorough…. + if ($result -ne 1) { + Write-Verbose "Remoting to $computerName returned an unexpected result." + return $false + } + $true + } + + Write-Verbose "Validating prereqs for remote script execution..." + if (!(Test-Path $FolderPath)) { + throw 'Folder path does not exist' + } elseif (!(Test-Path $ScriptPath)) { + throw 'Script path does not exist' + } elseif ((Get-ItemProperty -Path $ScriptPath).Extension -ne '.ps1') { + throw 'Script specified is not a Powershell script' + } elseif (!(Test-Ping $Computername)) { + throw 'Computer is not reachable' + } + $ScriptName = $ScriptPath | Split-Path -Leaf + $RemoteFolderPath = $FolderPath | Split-Path -Leaf + $RemoteScriptPath = "$RemoteDrive`:\$RemoteFolderPath\$ScriptName" +} + +process { + Write-Verbose "Copying the folder $FolderPath to the remote computer $ComputerName..." + Copy-Item $FolderPath -Recurse "\\$Computername\$RemoteDrive`$" -Force + Write-Verbose "Copying the script $ScriptName to the remote computer $ComputerName..." + Copy-Item $ScriptPath "\\$Computername\$RemoteDrive`$\$RemoteFolderPath" -Force + Write-Verbose "Executing $RemoteScriptPath on the remote computer $ComputerName..." + #Invoke-Command -ComputerName $Computername -ScriptBlock { Start-Process powershell.exe -ArgumentList $using:RemoteScriptPath } + ([WMICLASS]"\\$Computername\Root\CIMV2:Win32_Process").create("powershell.exe -File $RemoteScriptPath -NonInteractive -NoProfile") +} + +end { + #Write-Verbose "Cleaning up the copied folder and script from remote computer $Computername..." + #Remove-Item "\\$ComputerName\$RemoteDrive`$\$RemoteFolderPath" -Recurse -Force +} \ No newline at end of file diff --git a/Modules/CmSiteMaintenanceTasks.psm1 b/Modules/CmSiteMaintenanceTasks.psm1 new file mode 100644 index 0000000..0e33ec0 --- /dev/null +++ b/Modules/CmSiteMaintenanceTasks.psm1 @@ -0,0 +1,119 @@ +function Get-CmSiteMaintenanceTask { + <# + .SYNOPSIS + This function discovers and records the state of all site maintenance tasks on a ConfigMgr site server. + .PARAMETER TaskName + The name of the site maintenance task you'd like to limit the result set by. This accepts wildcards or + multiple names + .PARAMETER Status + The status (either enabled or disabled) of the site maintenance tasks you'd like to limit the result set by. + .PARAMETER SiteServer + The SCCM site server to query + .PARAMETER SiteCode + The SCCM site code + .EXAMPLE + + PS> Get-CmSiteMaintenanceTask -TaskName 'Disabled*' -Status Enabled + + This example finds all site maintenance tasks starting with 'Disabled' that are enabled. + #> + [CmdletBinding()] + [OutputType([System.Management.ManagementObject])] + param ( + [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)] + [string[]]$TaskName, + [Alias('ItemName')] + [ValidateSet('Enabled', 'Disabled')] + [string]$Status, + [string]$SiteServer = 'CONFIGMANAGER', + [ValidateLength(3, 3)] + [string]$SiteCode = 'UHP' + ) + + process { + try { + $WmiParams = @{ 'Computername' = $SiteServer; 'Namespace' = "root\sms\site_$SiteCode"} + + Write-Verbose -Message "Building the WMI query..." + if ($TaskName -or $Status) { + if ($TaskName) { + $WmiParams.Query = 'SELECT * FROM SMS_SCI_SQLTask WHERE ' + $NameConditions = @() + foreach ($n in $TaskName) { + ## Allow asterisks in cmdlet but WQL requires percentage and double backslashes + $NameValue = $n.Replace('*', '%').Replace('\', '\\') + $Operator = @{ $true = 'LIKE'; $false = '=' }[$NameValue -match '\%'] + $NameConditions += "(ItemName $Operator '$NameValue')" + } + $WmiParams.Query += ($NameConditions -join ' OR ') + } + if ($Status) { + $WmiParams.Class = 'SMS_SCI_SQLTask' + $Enabled = $Status -eq 'Enabled' + $WhereBlock = { $_.Enabled -eq $Enabled } + } + } else { + $WmiParams.Class = 'SMS_SCI_SQLTask' + } + if ($WhereBlock) { + Get-WmiObject @WmiParams | where $WhereBlock + } else { + Get-WmiObject @WmiParams + } + } catch { + Write-Error $_.Exception.Message + } + } +} + +function Enable-CmSiteMaintenanceTask { + <# + .SYNOPSIS + This function enables a ConfigMgr site maintenance task. + .PARAMETER InputObject + An object of returned from Get-CmSiteMaintenceTask of the task you'd like enabled. + .EXAMPLE + + PS> Get-CmSiteMaintenanceTask -TaskName 'Disabled*' -Status Disabled | Enable-CmsiteMaintenanceTask + + This example finds all site maintenance tasks starting with 'Disabled' that are disabled and enables them all. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline)] + [System.Management.ManagementObject]$InputObject + ) + process { + try { + $InputObject | Set-WmiInstance -Arguments @{ 'Enabled' = $true } | Out-Null + } catch { + Write-Error $_.Exception.Message + } + } +} + +function Disable-CmSiteMaintenanceTask { + <# + .SYNOPSIS + This function disables a ConfigMgr site maintenance task. + .PARAMETER InputObject + An object of returned from Get-CmSiteMaintenceTask of the task you'd like disabled. + .EXAMPLE + + PS> Get-CmSiteMaintenanceTask -TaskName 'Disabled*' -Status Enabled | Disable-CmsiteMaintenanceTask + + This example finds all site maintenance tasks starting with 'Disabled' that are enabled and disables them all. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline)] + [System.Management.ManagementObject]$InputObject + ) + process { + try { + $InputObject | Set-WmiInstance -Arguments @{ 'Enabled' = $false } | Out-Null + } catch { + Write-Error $_.Exception.Message + } + } +} \ No newline at end of file diff --git a/FileMonitor.psm1 b/Modules/FileMonitor.psm1 similarity index 100% rename from FileMonitor.psm1 rename to Modules/FileMonitor.psm1 diff --git a/PSModulePath.psm1 b/Modules/PSModulePath.psm1 similarity index 100% rename from PSModulePath.psm1 rename to Modules/PSModulePath.psm1 diff --git a/Modules/PSRemoting.psm1 b/Modules/PSRemoting.psm1 new file mode 100644 index 0000000..e6b84db --- /dev/null +++ b/Modules/PSRemoting.psm1 @@ -0,0 +1,45 @@ +#Requires -Version 4 + +function Test-PsRemoting { + <# + .SYNOPSIS + This function tests if PS remoting is enabled on a remote computer and (if used) will attempt + to connect with a local username and password. + .PARAMETER Computername + The remote computer name + .PARAMETER LocalCredential + An optional credential object representing the remote computer's local username and password + .PARAMETER DomainCredential + If the remote computer and the executing computer are both domain-joined, this is an optional + credential object representing a domain username and password to test against. By default, it + will test against the currently logged on user. + #> + [CmdletBinding(DefaultParameterSetName = 'None')] + param ( + [Parameter(Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [string[]]$Computername, + [Parameter(Mandatory,ParameterSetName = 'LocalCredential')] + [System.Management.Automation.PSCredential]$LocalCredential, + [Parameter(Mandatory, ParameterSetName = 'DomainCredential')] + [System.Management.Automation.PSCredential]$DomainCredential + ) + process { + foreach ($Computer in $Computername) { + try { + if (!$LocalCredential -and !$DomainCredential) { + ## Test remoting under the currently logged on user + Invoke-Command -ComputerName $Computer -ScriptBlock { 1 } + $true + } elseif ($LocalCredential) { ## Test remoting with a local username and password + + } else { ## Test remoting with a domain username and password + + } + } catch { + [pscustomobject]@{ 'Computer' = $Computer; 'Result' = $false; 'Error' = $_.Exception.Message } + } + } + } +} \ No newline at end of file diff --git a/New-CMMyApplication.ps1 b/New-CMMyApplication.ps1 new file mode 100644 index 0000000..9fec1d5 --- /dev/null +++ b/New-CMMyApplication.ps1 @@ -0,0 +1,168 @@ +<# +.SYNOPSIS + +.NOTES + Created on: 7/19/2014 + Created by: Adam Bertram + Filename: + Credits: + Requirements: + Todos: +.EXAMPLE + +.EXAMPLE + +.PARAMETER PARAM1 + +.PARAMETER PARAM2 + +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory)] + [string]$Name, + [Parameter(Mandatory)] + [string]$Manufacturer, + [Parameter(Mandatory)] + [ValidatePattern('[-+]?([0-9]*\.[0-9]+|[0-9]+)')] + [string]$SoftwareVersion, + [Parameter()] + [string]$Owner = 'Adam Bertram', + [Parameter()] + [string]$SupportContact = 'Adam Bertram', + [Parameter(Mandatory)] + [ValidateScript({ Test-Path -Path $_ -PathType Container })] + [string]$SourceFolderPath, + [Parameter(Mandatory)] + [string]$InstallationProgram, + [Parameter()] + [ValidateScript({ Test-Path -Path $_ -PathType Container })] + [string]$RootPackageFolderPath = '\\hosp.uhhg.org\dfs\softwarelibrary\software_packages', + [Parameter()] + [ValidateScript({ Test-Path -Path $_ -PathType 'Leaf' })] + [string]$IconLocationFilePath, + [Parameter()] + [string]$InstallationBehaviorType = 'InstallForSystem', + [Parameter()] + [string]$InstallationProgramVisibility = 'Hidden', + [Parameter()] + [string]$MaximumAllowedRunTimeMinutes = '15', + [Parameter()] + [string]$EstimatedInstallationTimeMinutes = '5', + [Parameter()] + [string]$RebootBehavior = 'ForceReboot', + [Parameter()] + [string]$DistributionPointGroup = 'All DPs', + [Parameter()] + [string]$SiteCode = 'UHP', + [Parameter()] + [string]$InstallScriptTemplateFilePath = '\\hosp.uhhg.org\dfs\softwarelibrary\software_packages\_Template_Files\install.ps1' + +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + try { + Write-Verbose 'Ensuring the ConfigurationManager module is available and importing it...' + ## Ensure the ConfigurationManager module is available + if (!(Test-Path "$(Split-Path $env:SMS_ADMIN_UI_PATH -Parent)\ConfigurationManager.psd1")) { + throw 'Configuration Manager module not found. Is the admin console intalled?' + } elseif (!(Get-Module 'ConfigurationManager')) { + Import-Module "$(Split-Path $env:SMS_ADMIN_UI_PATH -Parent)\ConfigurationManager.psd1" + } + Write-Verbose 'Performing prereq setup things...' + $Location = (Get-Location).Path + + ## Replace any spaces in the attributes with backspaces + $PackageFolderName = "$($Manufacturer.Replace(' ','_'))_$($Name.Replace(' ','_'))_$($SoftwareVersion.Replace(' ','_'))" + $ContentFolderPath = "$RootPackageFolderPath\$PackageFolderName" + } catch { + Write-Error $_.Exception.Message + } +} + +process { + try { + ## Create the source folder path to place the source content then copy all of the contents + ## from the original source folder to the package source folder. + Write-Verbose "Creating the content folder '$ContentFolderPath'..." + if (!(Test-Path $ContentFolderPath)) { + mkdir $ContentFolderPath | Out-Null + } else { + throw "The folder at '$ContentFolderPath' already exists." + } + + ## Copy source content to the content folder + Write-Verbose "Copying files from source folder '$SourceFolderPath' to '$ContentFolderPath'..." + Copy-Item -Path "$SourceFolderPath\*" -Destination $ContentFolderPath -Recurse -Force + + ## Copy the install.ps1 script template (import-module, $workingdir, start-log, etc) + Write-Verbose "Copying the installer template fiel at '$InstallScriptTemplateFilePath' to '$ContentFolderPath'..." + Copy-Item -Path $InstallScriptTemplateFilePath -Destination $ContentFolderPath + + ## Create the application container. This will hold our deployment type + Write-Verbose "Creating the application '$Name'..." + $NewCmApplicationParams = @{ + 'Name' = $Name; + 'Owner' = $Owner; + 'SupportContact' = $SupportContact; + 'IconLocationFile' = $IconLocationFilePath; + 'Publisher' = $Manufacturer; + 'SoftwareVersion' = $SoftwareVersion + } + Set-Location "$($SiteCode):" + New-CMApplication @NewCmApplicationParams | Out-Null + + ## Create the deployment type + Write-Verbose 'Creating the deployment type...' + $AddCmDeploymentTypeParams = @{ + 'ApplicationName' = $ApplicationName; + 'ScriptInstaller' = $true; + 'ManualSpecifyDeploymentType' = $true; + 'DeploymentTypeName' = "Deploy $Name"; + 'InstallationProgram' = $InstallationProgram; + 'ContentLocation' = $ContentFolderPath; + ## TODO: This param is ignored + ##'LogonRequirementType' = 'WhereOrNotUserLoggedOn'; + 'InstallationBehaviorType' = $InstallationBehaviorType; + 'InstallationProgramVisibility' = $InstallationProgramVisibility; + 'MaximumAllowedRunTimeMinutes' = $MaximumAllowedRunTimeMinutes; + 'EstimatedInstallationTimeMinutes' = $EstimatedInstallationTimeMinutes; + #'DetectDeploymentTypeByCustomScript' = $true; + #'ScriptType' = 'Powershell'; + #'ScriptContent' = ((Get-Content $InstallerDetectionScriptFilePath) -join "`r`n") + } + Add-CMDeploymentType @AddCmDeploymentTypeParams + + ## Set reboot behavior because it can't be set with Add-CMDeploymentType + Write-Verbose 'Setting reboot behavior for the deployment type...' + $SetCmDeploymentTypeParams = @{ + 'RebootBehavior' = $RebootBehavior; + 'DeploymentTypeName' = "Deploy $ApplicationName"; + 'ApplicationName' = $ApplicationName; + 'MsiOrScriptInstaller' = $true; + } + Set-CMDeploymentType @SetCmDeploymentTypeParams + + ## distribute to DPs + Write-Verbose 'Distributing content...' + Start-CMContentDistribution -DistributionPointGroupName $DistributionPointGroup -PackageName $Name + + ## Create the OSD package + Write-Verbose 'Creating the OSD package...' + $PackageConversionScriptFilePath = 'C:\Dropbox\Powershell\scripts\complete\Convert-CMApplicationToPackage.ps1' + if (Test-Path $PackageConversionScriptFilePath) { + & $PackageConversionScriptFilePath -ApplicationName $Name -SkipRequirements -DistributeContent -OsdFriendlyPowershellSyntax + } else { + Write-Warning "OSD package could not be created because the script '$PackageConversionScriptFilePath' does not exist" + } + + Write-Verbose 'Done.' + + #Unlock-CMObject $NewPackage + } catch { + Write-Error $_.Exception.Message + exit + } +} \ No newline at end of file diff --git a/New-Invoice/InvoiceTemplate.xml b/New-Invoice/InvoiceTemplate.xml new file mode 100644 index 0000000..8124898 --- /dev/null +++ b/New-Invoice/InvoiceTemplate.xml @@ -0,0 +1,16 @@ + + + + Adam Bertram + 555 Address Ln + MyCity, CA + 55555 + myemail@gmail.com + 555-444-3333 + + + + + + + diff --git a/New-Invoice/InvoiceTemplate.xsl b/New-Invoice/InvoiceTemplate.xsl new file mode 100644 index 0000000..fe9985e --- /dev/null +++ b/New-Invoice/InvoiceTemplate.xsl @@ -0,0 +1,312 @@ + + + + + <!DOCTYPE html> + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + +
+ diff --git a/New-Invoice/NReco.PdfGenerator.dll b/New-Invoice/NReco.PdfGenerator.dll new file mode 100644 index 0000000..6a46fd8 Binary files /dev/null and b/New-Invoice/NReco.PdfGenerator.dll differ diff --git a/New-Invoice/New-Invoice.ps1 b/New-Invoice/New-Invoice.ps1 new file mode 100644 index 0000000..7c1eda8 --- /dev/null +++ b/New-Invoice/New-Invoice.ps1 @@ -0,0 +1,213 @@ +<# +.SYNOPSIS + Creates a HTML, XML and PDF invoice. +.DESCRIPTION + Based off of a XML template, this script reads the template and adds any new field values + passed to it via parameters from this script. It then creates either a XML, HTML or PDF file + based on the extension of the output file you specify. +.NOTES + Created on: 11/15/2014 + Created by: Adam Bertram + Filename: New-Invoice.ps1 +.EXAMPLE + PS> .\New-Invoice.ps1 -InvoiceTitle 'New Title' -InvoiceNumber '12334' -ClientCompany 'new client' -ClientName 'new clent name' -Item @{'Description'='mydsc';'Price'='445';'Quantity'='6'} -Force -TopNote 'this is the top note' -BottomNote 'this is the bottom note' -InvoiceFilePath c:\invoice.pdf + + This example will create a PDF invoice in C:\ called invoice.pdf with a single item and will overwrite any invoice that exists in that location. +.PARAMETER InvoiceTitle + The title of the invoice +.PARAMETER InvoiceNumber + The invoice number +.PARAMETER ClientCompany + Your client's company name +.PARAMETER ClientName + The person in the company you'd like to send the invoice to +.PARAMETER Item + One or more items you'd like to add to the invoice. This can either be a single item as + a hashtable or multiple hashtables all with required keys of Price,Description and Quantity +.PARAMETER TopNote + The text to display above the item table +.PARAMETER BottomNote + The text to display below the item table +.PARAMETER Force + Use this to overwrite both file paths $XmlInvoiceFIlePath and $HtmlInvoiceFilePath. +.PARAMETER TemplateXmlFilePath + The file path to where the XML data used to prepopulate the invoice is +.PARAMETER TemplateXslFilePath + The file path to where the XSL styling sheet is located +.PARAMETER InvoiceFilePath + The file that will be output containing your invoice data +.LINK + http://www.psclistens.com/blog/2014/3/powershell-tip-convert-html-to-pdf.aspx + http://pdfgenerator.codeplex.com +#> +[CmdletBinding()] +[OutputType()] +param ( + [Parameter(Mandatory)] + [string]$InvoiceTitle, + [Parameter(Mandatory)] + [string]$InvoiceNumber, + [Parameter(Mandatory)] + [string]$ClientCompany, + [Parameter(Mandatory)] + [string]$ClientName, + [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] + [hashtable[]]$Item, + [Parameter()] + [string]$TopNote, + [Parameter()] + [string]$BottomNote, + [Parameter()] + [switch]$Force, + [Parameter()] + [ValidateScript({ Test-Path -Path $_ -PathType Leaf})] + [string]$TemplateXmlFilePath = "$PSScriptRoot\InvoiceTemplate.xml", + [Parameter()] + [ValidateScript({ Test-Path -Path $_ -PathType Leaf })] + [string]$TemplateXslFilePath = "$PSScriptRoot\InvoiceTemplate.xsl", + [Parameter()] + [string]$InvoiceFilePath = "$PSScriptRoot\Invoice.pdf" +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + try { + ## Check to ensure an invoice doesn't already exist if Force was not chosen + if (!$Force.IsPresent) { + if (Test-Path -Path $InvoiceFilePath -PathType Leaf) { + throw 'Existing invoice already exists at specified path. To overwrite, use the -Force parameter' + } + } + + function New-InvoiceItem ($ItemDescription, $ItemPrice, $ItemQuantity) { + ## Adds a new item to the invoice + $NewItem = $InvoiceXml.CreateElement("Item") + [void]$NewItem.AppendChild($InvoiceXml.CreateElement("Description")) + [void]$NewItem.AppendChild($InvoiceXml.CreateElement("Price")) + [void]$NewItem.AppendChild($InvoiceXml.CreateElement("Quantity")) + $NewItem.Description = $ItemDescription + $NewItem.Price = $ItemPrice + $NewItem.Quantity = $ItemQuantity + [void]$InvoiceXml.Invoice.AppendChild($NewItem) + } + + Function ConvertTo-PDF { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [string]$Html, + [Parameter()] + [string]$FileName + ) + begin { + $DllLoaded = $false + $PdfGenerator = "$PsScriptRoot\NReco.PdfGenerator.dll" + if (Test-Path $PdfGenerator) { + try { + $Assembly = [Reflection.Assembly]::LoadFrom($PdfGenerator) + $PdfCreator = New-Object NReco.PdfGenerator.HtmlToPdfConverter + $DllLoaded = $true + } catch { + Write-Error ('ConvertTo-PDF: Issue loading or using NReco.PdfGenerator.dll: {0}' -f $_.Exception.Message) + } + } else { + Write-Error ('ConvertTo-PDF: NReco.PdfGenerator.dll was not found.') + } + } + PROCESS { + if ($DllLoaded) { + $ReportOutput = $PdfCreator.GeneratePdf([string]$HTML) + Add-Content -Value $ReportOutput -Encoding byte -Path $FileName + } else { + Throw 'Error Occurred' + } + } + END { } + } + + +} catch { + Write-Error $_.Exception.Message + break +} +} + +process { + try { + Write-Verbose 'Building XML object' + ## Build the initial XML object with default values from template XML + Write-Verbose "Getting template XML file $TemplateXmlFilePath" + $script:InvoiceXml = [xml](Get-Content $TemplateXmlFilePath) + + ## Add per-invoice specific attributes to the XML object + $InvoiceXml.Invoice.InvoiceTitle = $InvoiceTitle + $InvoiceXml.Invoice.Date = (Get-Date).ToShortDateString() + $InvoiceXml.Invoice.ClientCompany = $ClientCompany + $InvoiceXml.Invoice.ClientName = $ClientName + $InvoiceXml.Invoice.InvoiceNumber = $InvoiceNumber + $InvoiceXml.Invoice.TopNote = $TopNote + $InvoiceXml.Invoice.BottomNote = $BottomNote + + ## Add each item to the invoice XML object + foreach ($i in $Item) { + if (!($i.ContainsKey('Description')) -or !($i.ContainsKey('Price')) -or !($i.ContainsKey('Quantity'))) { + Write-Warning "Item found that does not have all necessary fields to add to invoice" + } else { + Write-Verbose "Adding invoice item '$($i.Description)' to invoice" + New-InvoiceItem -ItemDescription $i.Description -ItemPrice $i.Price -ItemQuantity $i.Quantity + } + } + + ## Remove any existing invoices if they exist + Write-Verbose "Removing existing invoice '$InvoiceFilePath'" + Remove-Item -Path $InvoiceFilePath -Force -ea 'SilentlyContinue' + + ## Save the invoice XML object to file + if (([System.IO.FileInfo]$InvoiceFilePath).Extension -eq '.xml') { + $XmlOutput = $InvoiceFilePath + } else { + $XmlOutput = "$PsScriptRoot\TempXml.xml" + } + Write-Verbose "Saving XML file output as '$XmlOutput'" + $InvoiceXml.Save($XmlOutput) + + Write-Verbose "Loading XSL file '$TemplateXslFilePath'" + $xslt = New-Object System.Xml.Xsl.XslCompiledTransform + $xslt.Load($TemplateXslFilePath) + + if (([System.IO.FileInfo]$InvoiceFilePath).Extension -eq '.html') { + $HtmlOutput = $InvoiceFilePath + } else { + $HtmlOutput = "$PsScriptRoot\TempHtml.Html" + } + Write-Verbose "Transforming XML to HTML file '$HtmlOutput'" + $xslt.Transform($XmlOutput, $HtmlOutput) + + } catch { + Write-Error $_.Exception.Message + } +} +end { + try { + switch (([System.IO.FileInfo]$InvoiceFilePath).Extension) { + '.xml' { + Write-Verbose "XML output chosen. Removing temporary HTML file '$HtmlOutput'" + Remove-Item -Path $HtmlOutput -Force -ea 'SilentlyContinue' + } + '.html' { + Write-Verbose "HTML output chosen. Removing temporary XML file '$XmlOutput'" + Remove-Item -Path $XmlOutput -Force -ea 'SilentlyContinue' + } + '.pdf' { + Write-Verbose "Converting '$HtmlOutput' content to PDF file '$InvoiceFilePath'" + ConvertTo-PDF -Html (Get-Content $HtmlOutput -Raw) -FileName $InvoiceFilePath + Write-Verbose "PDF output chosen. Removing temporary HTML and XML files" + Remove-Item -Path $HtmlOutput -Force -ea 'SilentlyContinue' + Remove-Item -Path $XmlOutput -Force -ea 'SilentlyContinue' + } + } + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/New-Invoice/TempHtml.Html b/New-Invoice/TempHtml.Html new file mode 100644 index 0000000..75cb48c --- /dev/null +++ b/New-Invoice/TempHtml.Html @@ -0,0 +1,262 @@ + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
1600
600
+ + \ No newline at end of file diff --git a/New-Invoice/TempXml.xml b/New-Invoice/TempXml.xml new file mode 100644 index 0000000..f9ceedc --- /dev/null +++ b/New-Invoice/TempXml.xml @@ -0,0 +1,23 @@ + + + February Articles + Adam Bertram + 8616 Elizabeth Ln + Evansville, IN + 47725 + adbertram@gmail.com + 812-453-3658 + 1105 Media + Chris Paoli + 02242015 + 2/24/2015 + + + + + + Article + 200 + 3 + + \ No newline at end of file diff --git a/New-ScheduledScript.ps1 b/New-ScheduledScript.ps1 new file mode 100644 index 0000000..36cfdcb --- /dev/null +++ b/New-ScheduledScript.ps1 @@ -0,0 +1,180 @@ +<# +.SYNOPSIS + This script creates a scheduled task on a local or remote system to execute a Powershell script + based on a number of criteria. It is useful if you have a central server that you create multiple + different scheduled tasks on. +.NOTES + Created on: 1/20/15 + Created by: Adam Bertram + Filename: New-ScheduledScript.ps1 + Credits: http://blogs.technet.com/b/heyscriptingguy/archive/2015/01/16/use-powershell-to-create-scheduled-task-in-new-folder.aspx +.EXAMPLE + PS> New-ScheduledScript.ps1 -ScriptFilePath C:\callfromvbs.ps1 -LocalScriptFolderPath 'C:\' -TaskTriggerOptions @{'Daily' = $true; 'At' = '3Am'} -TaskName 'Test' -TaskRunAsUser 'lab.local\administrator' -TaskRunAsPassword 'mypassword' -Computername labdc.lab.local + + This script would copy the C:\callfromvbs.ps1 to the C:\ of labdc.lab.local. It would then create a scheduled task on labdc.lab.local + named Test that ran daily at 3AM under the lab.local\administrator credentials. +.PARAMETER ScriptFilePath + The remote or local file path of the Powershell script +.PARAMETER ScriptParameters + Any parameters to execute with the script +.PARAMETER LocalScriptFolderPath + If this script is copying a Powershell script from somewhere else, this is the folder path where the + script will be copied to and referenced to run in the scheduled task. +.PARAMETER TaskTriggerOptions + A hashtable of parameters that will be passed to the New-ScheduledTaskTrigger cmdlet. For available options, visit + http://technet.microsoft.com/en-us/library/jj649821.aspx. +.PARAMETER TaskName + The name of the scheduled task +.PARAMETER TaskRunAsUser + The username that the task will run under +.PARAMETER TaskRunAsPassword + The password to the runas user +.PARAMETER Computername + The name of the system in which the scheduled task will be created (if not the local system) +.PARAMETER PowershellRunAsArch + If your task scheduler machine is 64 bit this enforces the script to run under the 32 bit or 64 bit + Powershell host. By default, it will always run as 64 bit. +#> +[CmdletBinding()] +[OutputType([bool])] +param ( + [Parameter(Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [ValidatePattern('.*\.ps1$')] + [string]$ScriptFilePath, + [string]$ScriptParameters, + [Parameter(Mandatory)] + [string]$LocalScriptFolderPath, + [Parameter(Mandatory)] + [hashtable]$TaskTriggerOptions, + [Parameter(Mandatory)] + [string]$TaskName, + [Parameter(Mandatory)] + [string]$TaskRunAsUser, + [Parameter(Mandatory)] + [string]$TaskRunAsPassword, + [ValidateScript({ Test-Connection -ComputerName $_ -Quiet -Count 1})] + [string]$Computername = 'localhost', + [ValidateSet('x86', 'x64')] + [string]$PowershellRunAsArch +) + +begin { + $ErrorActionPreference = 'Stop' + Set-StrictMode -Version Latest + ## http://www.leeholmes.com/blog/2009/11/20/testing-for-powershell-remoting-test-psremoting/ + function Test-PsRemoting { + param ( + [Parameter(Mandatory)] + $computername + ) + + try { + $errorActionPreference = "Stop" + $result = Invoke-Command -ComputerName $computername { 1 } + } catch { + return $false + } + + ## I’ve never seen this happen, but if you want to be + ## thorough…. + if ($result -ne 1) { + Write-Verbose "Remoting to $computerName returned an unexpected result." + return $false + } + $true + } + + function Get-ComputerArchitecture { + if ((Get-CimInstance -ComputerName $Computername Win32_ComputerSystem -Property SystemType).SystemType -eq 'x64-based PC') { + 'x64' + } else { + 'x86' + } + } + + function Get-PowershellFilePath { + ## If user wants to run the script under the x86 host and the machine is x64 + if (($PowershellRunAsArch -eq 'x86') -and ((Get-ComputerArchitecture) -eq 'x64')) { + if ($Computername -eq 'localhost') { + ## If it's the localhost no need to do a WinRM query + "$($PsHome.Replace('System32','SysWow64'))\powershell.exe" + } else { + Invoke-Command -ComputerName $Computername -ScriptBlock { "$($PsHome.Replace('System32','SysWow64'))\powershell.exe" } + } + } else { + ## If the machine is 32 bit anyway or if it's 64 bit and the user wants 64 bit just use $PsHome + if ($Computername -eq 'localhost') { + ## If it's the localhost no need to do a WinRM query + "$PsHome\powershell.exe" + } else { + Invoke-Command -ComputerName $Computername -ScriptBlock { "$PsHome\powershell.exe" } + } + } + } + + function New-MyScheduledTask { + try { + $PowershellFilePath = Get-PowershellFilePath + if ($Computername -ne 'localhost') { + $ScriptBlock = { + $Action = New-ScheduledTaskAction -Execute $using:PowershellFilePath -Argument "-NonInteractive -NoLogo -NoProfile -File $using:ScriptFilePath $using:ScriptParameters" + $TaskTriggerOptions = $using:TaskTriggerOptions + $Trigger = New-ScheduledTaskTrigger @TaskTriggerOptions + $RunAsUser = $using:TaskRunAsUser + $Task = New-ScheduledTask -Action $Action -Trigger $Trigger -Settings (New-ScheduledTaskSettingsSet); + Register-ScheduledTask -TaskName $using:TaskName -InputObject $Task -User $using:TaskRunAsUser -Password $using:TaskRunAsPassword + } + $Params = @{ + 'Scriptblock' = $ScriptBlock + 'Computername' = $Computername + } + Invoke-Command @Params + } else { + $Action = New-ScheduledTaskAction -Execute $PowershellFilePath -Argument "-NonInteractive -NoLogo -NoProfile -File $ScriptFilePath" + $Trigger = New-ScheduledTaskTrigger @TaskTriggerOptions + $Task = New-ScheduledTask -Action $Action -Trigger $Trigger -Settings (New-ScheduledTaskSettingsSet); + Register-ScheduledTask -TaskName $TaskName -InputObject $Task -User $TaskRunAsUser -Password $TaskRunAsPassword + } + } catch { + Write-Error "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + $false + } + } + + function Get-UncPath ($HostName,$LocalPath) { + $NewPath = $LocalPath -replace (":", "$") + if ($NewPath.EndsWith("\")) { + $NewPath = [Regex]::Replace($NewPath, "\\$", "") + } + "\\$HostName\$NewPath" + } + + try { + if (($Computername -ne 'localhost') -and !(Test-PsRemoting -computername $Computername)) { + throw "PS remoting not available on the computer $Computername" + } + ## Ensure there's no trailing slash + $LocalScriptFolderPath = $LocalScriptFolderPath.TrimEnd('\') + } catch { + Write-Error "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + exit + } +} + +process { + try { + ## If we're not creating a task on the local computer or if the script to execute is not on the local computer + ## copy to the task scheduler system and change the script to execute to a local path + if ($ScriptFilePath.StartsWith('\\') -or ($Computername -ne 'localhost')) { + Copy-Item -Path $ScriptFilePath -Destination (Get-UncPath -HostName $Computername -LocalPath $LocalScriptFolderPath) + $ScriptFilePath = "$LocalScriptFolderPath\$($ScriptFilePath | Split-Path -Leaf)" + } + New-MyScheduledTask | Out-Null + $true + } catch { + Write-Error "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + $false + } +} \ No newline at end of file diff --git a/New-ValidationDynamicParam.ps1 b/New-ValidationDynamicParam.ps1 new file mode 100644 index 0000000..5cad4eb --- /dev/null +++ b/New-ValidationDynamicParam.ps1 @@ -0,0 +1,90 @@ +function Set-Example { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [ValidateScript({ Test-Path -Path $_ })] + [string]$Path, + [Parameter(Mandatory)] + [string]$Identity + ) + DynamicParam { + $ParamOptions = @( + @{ + 'Name' = 'Right'; + 'Mandatory' = $true; + 'ValidateSetOptions' = ([System.Security.AccessControl.FileSystemRights]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name + }, + @{ + 'Name' = 'InheritanceFlags'; + 'Mandatory' = $true; + 'ValidateSetOptions' = ([System.Security.AccessControl.InheritanceFlags]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name + }, + @{ + 'Name' = 'PropagationFlags'; + 'Mandatory' = $true; + 'ValidateSetOptions' = ([System.Security.AccessControl.PropagationFlags]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name + }, + @{ + 'Name' = 'Type'; + 'Mandatory' = $true; + 'ValidateSetOptions' = ([System.Security.AccessControl.AccessControlType]).DeclaredMembers | where { $_.IsStatic } | select -ExpandProperty name + } + ) + $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + foreach ($Param in $ParamOptions) { + $RuntimeParam = New-ValidationDynamicParam @Param + $RuntimeParamDic.Add($Param.Name, $RuntimeParam) + } + + return $RuntimeParamDic + } + + begin { + $PsBoundParameters.GetEnumerator() | foreach { New-Variable -Name $_.Key -Value $_.Value -ea 'SilentlyContinue' } + } + + process { + try { + $Acl = Get-Acl $Path + #$Ar = New-Object System.Security.AccessControl.FileSystemAccessRule('Everyone', 'FullControl', 'ContainerInherit,ObjectInherit', 'NoPropagateInherit', 'Allow') + $Ar = New-Object System.Security.AccessControl.FileSystemAccessRule($Identity, $Right, $InheritanceFlags, $PropagationFlags, $Type) + $Acl.SetAccessRule($Ar) + Set-Acl $Path $Acl + } catch { + Write-Error $_.Exception.Message + } + } +} + +function New-ValidationDynamicParam { + [CmdletBinding()] + [OutputType('System.Management.Automation.RuntimeDefinedParameter')] + param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$Name, + [ValidateNotNullOrEmpty()] + [Parameter(Mandatory)] + [array]$ValidateSetOptions, + [Parameter()] + [switch]$Mandatory = $false, + [Parameter()] + [string]$ParameterSetName = '__AllParameterSets', + [Parameter()] + [switch]$ValueFromPipeline = $false, + [Parameter()] + [switch]$ValueFromPipelineByPropertyName = $false + ) + + $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute + $ParamAttrib.Mandatory = $Mandatory.IsPresent + $ParamAttrib.ParameterSetName = $ParameterSetName + $ParamAttrib.ValueFromPipeline = $ValueFromPipeline.IsPresent + $ParamAttrib.ValueFromPipelineByPropertyName = $ValueFromPipelineByPropertyName.IsPresent + $AttribColl.Add($ParamAttrib) + $AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($ValidateSetOptions))) + $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Name, [string], $AttribColl) + $RuntimeParam + +} \ No newline at end of file diff --git a/PacketCapture.ps1 b/PacketCapture.ps1 new file mode 100644 index 0000000..156c791 --- /dev/null +++ b/PacketCapture.ps1 @@ -0,0 +1,112 @@ +#region function Start-PacketTrace +function Start-PacketTrace { +<# + .SYNOPSIS + This function starts a packet trace using netsh. Upon completion, it will begin capture all + packets coming into and leaving the local computer and will continue to do do until + Stop-PacketCapture is executed. + .EXAMPLE + PS> Start-PacketTrace -TraceFilePath C:\Tracefile.etl + + This example will begin a packet capture on the local computer and place all activity + in the ETL file C:\Tracefile.etl. + + .PARAMETER TraceFilePath + The file path where the trace file will be placed and recorded to. This file must be an ETL file. + + .PARAMETER Force + Use the Force parameter to overwrite the trace file if one exists already + + .INPUTS + None. You cannot pipe objects to Start-PacketTrace. + + .OUTPUTS + None. Start-PacketTrace returns no output upon success. +#> + [CmdletBinding()] + [OutputType()] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-Path -Path ($_ | Split-Path -Parent) -PathType Container })] + [ValidatePattern('.*\.etl$')] + [string[]]$TraceFilePath, + [Parameter()] + [switch]$Force + ) + begin { + Set-StrictMode -Version Latest + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + } + process { + try { + if (Test-Path -Path $TraceFilePath -PathType Leaf) { + if (-not ($Force.IsPresent)) { + throw "An existing trace file was found at [$($TraceFilePath)] and -Force was not used. Exiting.." + } else { + Remove-Item -Path $TraceFilePath + } + } + $OutFile = "$PSScriptRoot\temp.txt" + $Process = Start-Process "$($env:windir)\System32\netsh.exe" -ArgumentList "trace start persistent=yes capture=yes tracefile=$TraceFilePath" -RedirectStandardOutput $OutFile -Wait -NoNewWindow -PassThru + if ($Process.ExitCode -notin @(0, 3010)) { + throw "Failed to start the packet trace. Netsh exited with an exit code [$($Process.ExitCode)]" + } else { + Write-Verbose -Message "Successfully started netsh packet capture. Capturing all activity to [$($TraceFilePath)]" + } + } catch { + Write-Error -Message "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + } finally { + if (Test-Path -Path $OutFile -PathType Leaf) { + Remove-Item -Path $OutFile + } + } + } +} +#endregion function Start-PacketTrace + +#region function Stop-PacketTrace +function Stop-PacketTrace { +<# + .SYNOPSIS + This function stops a packet trace that is currently running using netsh. + .EXAMPLE + PS> Stop-PacketTrace + + This example stops any running netsh packet capture. + .INPUTS + None. You cannot pipe objects to Stop-PacketTrace. + + .OUTPUTS + None. Stop-PacketTrace returns no output upon success. +#> + [CmdletBinding()] + [OutputType()] + param + () + begin { + Set-StrictMode -Version Latest + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + } + process { + try { + $OutFile = "$PSScriptRoot\temp.txt" + $Process = Start-Process "$($env:windir)\System32\netsh.exe" -ArgumentList 'trace stop' -Wait -NoNewWindow -PassThru -RedirectStandardOutput $OutFile + if ((Get-Content $OutFile) -eq 'There is no trace session currently in progress.'){ + Write-Verbose -Message 'There are no trace sessions currently in progress' + } elseif ($Process.ExitCode -notin @(0, 3010)) { + throw "Failed to stop the packet trace. Netsh exited with an exit code [$($Process.ExitCode)]" + } else { + Write-Verbose -Message 'Successfully stopped netsh packet capture' + } + } catch { + Write-Error -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + } finally { + if (Test-Path -Path $OutFile -PathType Leaf) { + Remove-Item -Path $OutFile + } + } + } +} +#endregion function Stop-PacketTrace \ No newline at end of file diff --git a/PostMessageToCampfire.ps1 b/PostMessageToCampfire.ps1 new file mode 100644 index 0000000..d5189ff --- /dev/null +++ b/PostMessageToCampfire.ps1 @@ -0,0 +1,10 @@ +$RoomNumber = '407559' +$postUrl = "https://adambertram.campfirenow.com/room/$RoomNumber/speak.json" +$body = 'test message' +$token = '25f1733faf39d63ac8a860430fc18448df179b28' + +$message = 'TextMessage'+$body+'' +$baseuri = $base_url + '/speak.xml' +$contentType = 'application/xml' +$headers = @{"Authorization" = "Basic "+[System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($token+": x"))} +Invoke-WebRequest -Uri $postUrl -Headers $headers -Method Post -body $message -contenttype 'application/xml' \ No newline at end of file diff --git a/Remove-AutoConfigProxy.ps1 b/Remove-AutoConfigProxy.ps1 new file mode 100644 index 0000000..42e0a84 --- /dev/null +++ b/Remove-AutoConfigProxy.ps1 @@ -0,0 +1,200 @@ +<# +.SYNOPSIS + This script nullifies the AutoConfigURL registry value in the HKCU hive for the current and all + other users. This removes the auto configuration proxy URL value in IE. +.NOTES + Created on: 5/7/2015 + Created by: Adam Bertram + Filename: Remove-AutoConfigProxy.ps1 +.EXAMPLE + PS> .\Remove-AutoConfigProxy.ps1 + + This nullifies the AutoConfigURL HKCU registry value for the current and all other user profiles to + remove the IE auto configuration URL. + +#> +[CmdletBinding()] +param () +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + + function Get-LoggedOnUserSID { + <# + .SYNOPSIS + This function queries the registry to find the SID of the user that's currently logged onto the computer interactively. + #> + [CmdletBinding()] + param () + process { + try { + if (-not (Get-PSDrive -Name HKU -ErrorAction SilentlyContinue)) { + New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null + } + (Get-ChildItem HKU: | where { $_.Name -match 'S-\d-\d+-(\d+-){1,14}\d+$' }).PSChildName + } catch { + Write-Warning -Message "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + } + } + } + + function Set-RegistryValueForAllUsers { + <# + .SYNOPSIS + This function sets a registry value in every user profile hive. + .EXAMPLE + PS> Set-RegistryValueForAllUsers -RegistryInstance @{'Name' = 'Setting'; 'Type' = 'String'; 'Value' = 'someval'; 'Path' = 'SOFTWARE\Microsoft\Windows\Something'} + + This example would modify the string registry value 'Type' in the path 'SOFTWARE\Microsoft\Windows\Something' to 'someval' + for every user registry hive. + .PARAMETER RegistryInstance + A hash table containing key names of 'Name' designating the registry value name, 'Type' to designate the type + of registry value which can be 'String,Binary,Dword,ExpandString or MultiString', 'Value' which is the value itself of the + registry value and 'Path' designating the parent registry key the registry value is in. + .PARAMETER Remove + A switch parameter that overrides the default setting to only change or add registry values. This removes one of more registry keys instead. + If this parameter is used the only required key in the RegistryInstance parameter is Path. This will automatically remove both + the x86 and x64 paths if the key is a child under the SOFTWARE key. There's no need to specify the WOW6432Node path also. + .PARAMETER Force + A switch parameter that is used if the registry value key path doesn't exist will create the entire parent/child key hierachy and creates the + registry value. If this parameter is not used, if the key the value is supposed to be in does not exist the function will skip the value. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [hashtable[]]$RegistryInstance, + [switch]$Remove + ) + begin { + if (-not (Get-PSDrive -Name HKU -ErrorAction SilentlyContinue)) { + New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null + } + } + process { + try { + ## Change the registry values for the currently logged on user + $LoggedOnSids = Get-LoggedOnUserSID + if ($LoggedOnSids -is [string]) { + Write-Verbose -Message "Found 1 logged on user SID" + } else { + Write-Verbose -Message "Found $($LoggedOnSids.Count) logged on user SIDs" + } + foreach ($sid in $LoggedOnSids) { + Write-Verbose -Message "Loading the user registry hive for the logged on SID $sid" + foreach ($instance in $RegistryInstance) { + if ($Remove.IsPresent) { + Write-Verbose -Message "Removing registry key '$($instance.path)'" + Remove-Item -Path "HKU:\$sid\$($instance.Path)" -Recurse -Force -ea 'SilentlyContinue' + } else { + if (!(Get-Item -Path "HKU:\$sid\$($instance.Path)" -ea 'SilentlyContinue')) { + Write-Verbose -Message "The registry key HKU:\$sid\$($instance.Path) does not exist. Creating..." + New-Item -Path "HKU:\$sid\$($instance.Path | Split-Path -Parent)" -Name ($instance.Path | Split-Path -Leaf) -Force | Out-Null + } else { + Write-Verbose -Message "The registry key HKU:\$sid\$($instance.Path) already exists. No need to create." + } + Write-Verbose -Message "Setting registry value $($instance.Name) at path HKU:\$sid\$($instance.Path) to $($instance.Value)" + + Set-ItemProperty -Path "HKU:\$sid\$($instance.Path)" -Name $instance.Name -Value $instance.Value -Type $instance.Type -Force + } + } + } + + foreach ($instance in $RegistryInstance) { + if ($Remove.IsPresent) { + if ($instance.Path.Split('\')[0] -eq 'SOFTWARE' -and ((Get-Architecture) -eq 'x64')) { + $Split = $instance.Path.Split('\') + $x86Path = "HKCU\SOFTWARE\Wow6432Node\{0}" -f ($Split[1..($Split.Length)] -join '\') + $CommandLine = "reg delete `"{0}`" /f && reg delete `"{1}`" /f" -f "HKCU\$($instance.Path)", $x86Path + } else { + $CommandLine = "reg delete `"{0}`" /f" -f "HKCU\$($instance.Path)" + } + } else { + ## Convert the registry value type to one that reg.exe can understand + switch ($instance.Type) { + 'String' { + $RegValueType = 'REG_SZ' + } + 'Dword' { + $RegValueType = 'REG_DWORD' + } + 'Binary' { + $RegValueType = 'REG_BINARY' + } + 'ExpandString' { + $RegValueType = 'REG_EXPAND_SZ' + } + 'MultiString' { + $RegValueType = 'REG_MULTI_SZ' + } + default { + throw "Registry type '$($instance.Type)' not recognized" + } + } + if (!(Get-Item -Path "HKCU:\$($instance.Path)" -ea 'SilentlyContinue')) { + Write-Verbose -Message "The registry key 'HKCU:\$($instance.Path)'' does not exist. Creating..." + New-Item -Path "HKCU:\$($instance.Path) | Split-Path -Parent)" -Name ("HKCU:\$($instance.Path)" | Split-Path -Leaf) -Force | Out-Null + } + if (-not $instance.Value) { + $instance.Value = '""' + } + $CommandLine = "reg add `"{0}`" /v {1} /t {2} /d {3} /f" -f "HKCU\$($instance.Path)", $instance.Name, $RegValueType, $instance.Value + } + Set-AllUserStartupAction -CommandLine $CommandLine + } + } catch { + Write-Warning -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + $false + } + } + } + + function Set-AllUserStartupAction { + <# + .SYNOPSIS + A function that executes a command line for the any current logged on user and uses the Active Setup registry key to set a + registry value that contains a command line EXE with arguments that will be executed once for every user that logs in. + .PARAMETER CommandLine + The command line string that will be executed once at every user logon + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$CommandLine + ) + process { + try { + ## Create the Active Setup registry key so that the reg add cmd will get ran for each user + ## logging into the machine. + ## http://www.itninja.com/blog/view/an-active-setup-primer + $Guid = [guid]::NewGuid().Guid + Write-Verbose -Message "Created GUID '$Guid' to use for Active Setup" + $ActiveSetupRegParentPath = 'HKLM:\Software\Microsoft\Active Setup\Installed Components' + New-Item -Path $ActiveSetupRegParentPath -Name $Guid -Force | Out-Null + $ActiveSetupRegPath = "HKLM:\Software\Microsoft\Active Setup\Installed Components\$Guid" + Write-Verbose -Message "Using registry path '$ActiveSetupRegPath'" + Write-Verbose -Message "Setting command line registry value to '$CommandLine'" + Set-ItemProperty -Path $ActiveSetupRegPath -Name '(Default)' -Value 'All Users Startup Action' -Force + Set-ItemProperty -Path $ActiveSetupRegPath -Name 'Version' -Value '1' -Force + Set-ItemProperty -Path $ActiveSetupRegPath -Name 'StubPath' -Value $CommandLine -Force + Write-Verbose -Message 'Done' + } catch { + Write-Warning -Message "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + $false + } + } + } +} + +process { + try { + $Instance = @{ + 'Name' = 'AutoConfigURL'; + 'Type' = 'String'; + 'Path' = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings'; + 'Value' = '' + } + Set-RegistryValueForAllUsers -RegistryInstance $Instance + } catch { + Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + } +} \ No newline at end of file diff --git a/Remove-CMDirectMembershipRule.ps1 b/Remove-CMDirectMembershipRule.ps1 new file mode 100644 index 0000000..ac893c9 --- /dev/null +++ b/Remove-CMDirectMembershipRule.ps1 @@ -0,0 +1,84 @@ +<# +.NOTES + Created on: 5/22/2014 10:56 AM + Created by: Adam Bertram + Filename: Remove-CMDirectMembershipRule.ps1 + Credits: http://www.david-obrien.net/2013/02/24/remove-direct-membership-rules-configmgr/ +.DESCRIPTION + This script removes all direct membership rules from a specified collection name +.EXAMPLE + .\Remove-CMDirectMembershipRule.ps1 -SiteCode 'CON' -SiteServer 'SERVERNAME' -CollectionName 'NAMEHERE' +.PARAMETER SiteCode + Your Configuration Manager site code +.PARAMETER SiteServer + Your Configuration Manager site server name +.PARAMETER CollectionName + The collection name you'd like to remove direct membership rules from +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory = $False, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $True)] + [string]$SiteCode = 'UHP', + [Parameter(Mandatory = $False, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $True)] + [string]$SiteServer = 'CONFIGMANAGER', + [Parameter(Mandatory = $True, + ValueFromPipeline = $True, + ValueFromPipelineByPropertyName = $True)] + [string]$CollectionName +) + +begin { + try { + if ([Environment]::Is64BitProcess) { + # this script needs to run in a x86 shell, but we need to access the x64 reg-hive to get the AdminConsole install directory + throw 'This script must be run in a x86 shell.' + } + $ConfigMgrModule = "$($env:SMS_ADMIN_UI_PATH | Split-Path -Parent)\ConfigurationManager.psd1" + if (!(Test-Path $ConfigMgrModule)) { + throw 'Configuration Manager module not found in admin console path' + } + Import-Module $ConfigMgrModule + + $BeforeLocation = (Get-Location).Path + } catch { + Write-Error $_.Exception.Message + } +} + +process { + try { + Set-Location "$SiteCode`:" + $CommonWmiParams = @{ + 'ComputerName' = $SiteServer + 'Namespace' = "root\sms\site_$SiteCode" + } + #HACK: This should be 1 WQL query using JOIN but it's not immediately obvious why that doesn't work + #Get-WmiObject -ComputerName $SiteServer -Namespace "ROOT\sms\site_$SiteCode" -Query "SELECT DISTINCT * FROM SMS_CollectionMember_a AS collmem JOIN SMS_Collection AS coll ON coll.CollectionID = collmem.CollectionID" + + $CollectionId = Get-WmiObject @CommonWmiParams -Query "SELECT CollectionID FROM SMS_Collection WHERE Name = '$CollectionName'" | select -ExpandProperty CollectionID + if (!$CollectionId) { + throw "No collection found with the name $CollectionName" + } + + ## Find the collection members + $CollectionMembers = Get-WmiObject @CommonWmiParams -Query "SELECT Name FROM SMS_CollectionMember_a WHERE CollectionID = '$CollectionId'" | Select -ExpandProperty Name + + if (!$CollectionMembers) { + Write-Warning 'No collection members found in collection' + } else { + foreach ($member in $CollectionMembers) { + Remove-CMDeviceCollectionDirectMembershipRule -CollectionID $CollectionID -ResourceName $member -force + } + } + } catch { + Write-Error $_.Exception.Message + } +} + +end { + Set-Location $BeforeLocation +} \ No newline at end of file diff --git a/Restore-AzureVMSnapshot.ps1 b/Restore-AzureVMSnapshot.ps1 new file mode 100644 index 0000000..d4e983e --- /dev/null +++ b/Restore-AzureVMSnapshot.ps1 @@ -0,0 +1,145 @@ +<# +.SYNOPSIS + Restores a blob copy (snapshot) of all disks used by an Azure VM +.NOTES + Created on: 7/6/2014 + Created by: Adam Bertram + Filename: Restore-AzureVmSnapshot.ps1 + Credits: http://bit.ly/1r1umit + Requirements: Azure IaaS VM +.EXAMPLE + Restore-AzureVmSnapshot -AzureVM CCM1 -ServiceName CLOUD + This example will create a blob copy of all disks in the VM CCM1, Service name CLOUD +.EXAMPLE + Create-AzureVmSnapshot -AzureVM 'CCM1','CCM2' -Overwrite + This example creates blob copies for all disks in the VMs CCM1 and CCM2 using the default + service name parameter and if any existing copies are detected, automatically overwrite them. +.PARAMETER AzureVM + The name of the Azure VM. Multiple VM names are supported. +.PARAMETER ServiceName + The name of your Azure cloud service. +.PARAMETER VmExportConfigFolderPath + The VM has to be removed in order to restore the snapshot. This is the directory on the local + computer that will house the export VM configuration XMLs to be used to restore the VM afterwards. +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory, + ValueFromPipeline)] + [string[]]$AzureVM, + [string]$ServiceName = 'ADBCLOUD', + [string]$VmExportConfigFolderPath = 'C:\ExportedVMs' +) + +begin { + Set-StrictMode -Version Latest + try { + ## Ensure the module is available and import it + $AzureModuleFilePath = "$($env:ProgramFiles)\Microsoft SDKs\Windows Azure\PowerShell\ServiceManagement\Azure\Azure.psd1" + if (!(Test-Path $AzureModuleFilePath)) { + Write-Error 'Azure module not found' + } else { + Import-Module $AzureModuleFilePath + } + + $script:BackupContainer = 'backups' + + ## Ensure a path to store the VM configurations to reprovision the VM later + if (!(Test-Path $VmExportConfigFolderPath -PathType Container)) { + New-Item -Path $VmExportConfigFolderPath -ItemType Directory | Out-Null + } + + function Restore-Snapshot([Microsoft.WindowsAzure.Commands.ServiceManagement.Model.PersistentVMModel.OSVirtualHardDisk]$Disk) { + $DiskName = $Disk.DiskName + $DiskUris = $Disk.MediaLink + $StorageAccount = $DiskUris.Host.Split('.')[0] + $Blob = $Disk.MediaLink.Segments[-1] + $Container = $Disk.MediaLink.Segments[-2].TrimEnd('/') + + While ((Get-AzureDisk -DiskName $DiskName).AttachedTo) { + Write-Verbose "Waiting for $DiskName to detach..." + Start-Sleep 5 + } + Remove-AzureDisk -DiskName $DiskName -DeleteVHD + + $BlobCopyParams = @{ + 'SrcContainer' = $BackupContainer; + 'SrcBlob' = $Blob; + 'DestContainer' = $Container; + 'Force' = $true + } + Start-AzureStorageBlobCopy @BlobCopyParams + Get-AzureStorageBlobCopyState -Container $Container -Blob $Blob -WaitForComplete + Add-AzureDisk -DiskName $DiskName -MediaLocation $DiskUris.AbsoluteUri -OS 'Windows' + } + + } catch { + Write-Error $_.Exception.Message + exit + } +} + +process { + try { + foreach ($Vm in $AzureVM) { + $Vm = Get-AzureVM -ServiceName $ServiceName -Name $Vm + if ($Vm.Status -ne 'StoppedVM') { + if ($Vm.Status -eq 'ReadyRole') { + Write-Verbose "VM $($Vm.Name) is started. Bringing down into a provisioned state" + ## Bring the VM down in a provisioned state + $Vm | Stop-AzureVm -StayProvisioned + } elseif ($Vm.Status -eq 'StoppedDeallocated') { + Write-Verbose "VM $($Vm.Name) is stopped but not in a provisioned state." + ## Bring up the VM and bring it back down in a provisioned state + Write-Verbose "Starting up VM $($Vm.Name)..." + $Vm | Start-AzureVm + while ((Get-AzureVm -ServiceName $ServiceName -Name $Vm.Name).Status -ne 'ReadyRole') { + sleep 5 + Write-Verbose "Waiting on VM $($Vm.Name) to be in a ReadyRole state..." + } + Write-Verbose "VM $($Vm.Name) now up. Bringing down into a provisioned state..." + $Vm | Stop-AzureVm -StayProvisioned + } + + } + + $OsDisk = $Vm | Get-AzureOSDisk + + ## Export the VM's configuration + $VmExportPath = "$VmExportConfigFolderPath\$($Vm.Name).xml" + $Vm | Export-AzureVM -Path $VmExportPath + + ## Remove the VM + Remove-AzureVM -ServiceName $Vm.ServiceName -Name $Vm.Name + + ## Restore a snapshot of OS disk + Restore-Snapshot -Disk $OsDisk + + ## Restore snapshots of all data disks + $DataDisks = $Vm | Get-AzureDataDisk + if ($DataDisks) { + Write-Verbose "Data disks found on VM. Restoring..." + foreach ($DataDisk in $DataDisks) { + Restore-Snapshot -Disk $DataDisk + } + } + + Import-AzureVM -Path $VmExportPath | New-AzureVM -ServiceName $Vm.ServiceName + While ((Get-AzureVM -Name $Vm.Name).Status -ne 'ReadyRole') { + Write-Verbose "Waiting for $($Vm.Name) to become available again..." + Start-Sleep 5 + } + } + } catch { + Write-Error $_.Exception.Message + exit + } +} + +end { + try { + + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/SCOMMonitors/FileCountInDirectory.ps1 b/SCOMMonitors/FileCountInDirectory.ps1 new file mode 100644 index 0000000..ab23815 --- /dev/null +++ b/SCOMMonitors/FileCountInDirectory.ps1 @@ -0,0 +1,23 @@ +param ( + [string]$DirectoryName, + [int]$MaxFileCount +) + +$API = New-Object -ComObject 'MOM.ScriptAPI' +$PropertyBag = $API.CreatePropertyBag() + +try { + $FileCount = (Get-ChildItem -Path $DirectoryName | Where-Object { !$_.PsIsContainer }).Count + if ($FileCount -ge $MaxFileCount) { + $PropertyBag.AddValue('State', 'Critical') + $PropertyBag.Addvalue('Description', "There are $($FileCount - $MaxFileCount) more files than what should be in the directory $DirectoryName") + } else { + $PropertyBag.AddValue('State', 'Healthy') + $PropertyBag.AddValue('Description', "There are less than $MaxFileCount files accumulated in the directory $DirectoryName") + } + $PropertyBag +} catch { + $PropertyBag.AddValue('State', 'Warning') + $PropertyBag.Addvalue('Description', $_.Exception.Message) + $PropertyBag +} diff --git a/SCOMMonitors/WindowsProcessExists.ps1 b/SCOMMonitors/WindowsProcessExists.ps1 new file mode 100644 index 0000000..2aa51d2 --- /dev/null +++ b/SCOMMonitors/WindowsProcessExists.ps1 @@ -0,0 +1,21 @@ +param ([string]$ProcessName) + +$API = New-Object -ComObject 'MOM.ScriptAPI' +$PropertyBag = $API.CreatePropertyBag() + +try { + $Process = Get-WmiObject -Class 'Win32_Process' -Filter "Name = '$ProcessName'" + if (!$Process) { + $PropertyBag.AddValue('State', 'Critical') + $PropertyBag.Addvalue('Description', "The process '$ProcessName' is not running.") + } else { + $PropertyBag.AddValue('State', 'Healthy') + $PropertyBag.AddValue('Description', "The process '$ProcessName' is running.") + } + $PropertyBag +} catch { + $PropertyBag.AddValue('State', 'Warning') + $PropertyBag.Addvalue('Description', $_.Exception.Message) + $PropertyBag +} + diff --git a/Send-TwitterDirectMessage.ps1 b/Send-TwitterDirectMessage.ps1 new file mode 100644 index 0000000..2a850cc --- /dev/null +++ b/Send-TwitterDirectMessage.ps1 @@ -0,0 +1,61 @@ +$VerbosePreference = 'continue' + +## Works with Twitter API v1.1 +[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null +[Reflection.Assembly]::LoadWithPartialName("System.Net") | Out-Null + +$status = [System.Uri]::EscapeDataString("test") +$oauth_consumer_key = "qE0WtEleJzp8ErWI82B3Jw8gl" +$oauth_consumer_secret = "r06813yKNEjaErJc4mkacWmFSNwnT1H05Yyjd8XQ5bOhKxVBVc" +$oauth_token = "19891458-D4dvUVxHpHMCiTppg7OF4kPKgQQXM2O8rhXMFiNVc" +$oauth_token_secret = "ZPLaltGH39jdYagotyQL5C9IYAKty6xovhBUcvPc5IYbr" +$oauth_nonce = [System.Convert]::ToBase64String(([System.Text.Encoding]::ASCII.GetBytes("$([System.DateTime]::Now.Ticks.ToString())12345"))).Replace('=','g') +Write-Verbose "Using oauth_nonce of $oauth_nonce" +$ts = [System.DateTime]::UtcNow - [System.DateTime]::ParseExact("01/01/1970", "dd/MM/yyyy", $null) +$oauth_timestamp = [System.Convert]::ToInt64($ts.TotalSeconds).ToString(); +Write-Verbose "Oauth timestamp is $oauth_timestamp" + +## Keys must be in alphabetical order +$signature = "POST&"; +$signature += [System.Uri]::EscapeDataString("https://api.twitter.com/1.1/statuses/update.json") + "&" +$signature += [System.Uri]::EscapeDataString("oauth_consumer_key=" + $oauth_consumer_key + "&"); +$signature += [System.Uri]::EscapeDataString("oauth_nonce=" + $oauth_nonce + "&"); +$signature += [System.Uri]::EscapeDataString("oauth_signature_method=HMAC-SHA1&"); +$signature += [System.Uri]::EscapeDataString("oauth_timestamp=" + $oauth_timestamp + "&"); +$signature += [System.Uri]::EscapeDataString("oauth_token=" + $oauth_token + "&"); +$signature += [System.Uri]::EscapeDataString("oauth_version=1.0&"); +$signature += [System.Uri]::EscapeDataString("status=" + $status); +Write-Verbose "Unencrypted signature = $signature" + +$signature_key = [System.Uri]::EscapeDataString($oauth_consumer_secret) + "&" + [System.Uri]::EscapeDataString($oauth_token_secret); + +$hmacsha1 = new-object System.Security.Cryptography.HMACSHA1; +$hmacsha1.Key = [System.Text.Encoding]::ASCII.GetBytes($signature_key); +$oauth_signature = [System.Convert]::ToBase64String($hmacsha1.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($signature))); +Write-Verbose "Using the signature '$oauth_signature'" + +$oauth_authorization = 'OAuth '; +$oauth_authorization += 'oauth_consumer_key="' + [System.Uri]::EscapeDataString($oauth_consumer_key) + '", '; +$oauth_authorization += 'oauth_nonce="' + [System.Uri]::EscapeDataString($oauth_nonce) + '", '; +$oauth_authorization += 'oauth_signature="' + [System.Uri]::EscapeDataString($oauth_signature) + '", '; +$oauth_authorization += 'oauth_signature_method="HMAC-SHA1",' +$oauth_authorization += 'oauth_timestamp="' + [System.Uri]::EscapeDataString($oauth_timestamp) + '", ' +$oauth_authorization += 'oauth_token="' + [System.Uri]::EscapeDataString($oauth_token) + '", '; +$oauth_authorization += 'oauth_version="1.0"'; +Write-Verbose "Authorization string is $oauth_authorization" + +$post_body = [System.Text.Encoding]::ASCII.GetBytes("status=" + $status); + +Invoke-RestMethod -URI 'https://api.twitter.com/1.1/statuses/update.json' -Method Post -Body $post_body -Headers @{ 'Authorization' = $oauth_authorization } -ContentType "application/x-www-form-urlencoded" + +<# +[System.Net.HttpWebRequest] $request = [System.Net.WebRequest]::Create("https://api.twitter.com/1.1/statuses/update.json"); +$request.Method = "POST"; +$request.Headers.Add("Authorization", $oauth_authorization); +$request.ContentType = "application/x-www-form-urlencoded"; +$body = $request.GetRequestStream(); +$body.write($post_body, 0, $post_body.length); +$body.flush(); +$body.close(); +$response = $request.GetResponse(); +#> \ No newline at end of file diff --git a/Send-TwitterDirectMessagev2.ps1 b/Send-TwitterDirectMessagev2.ps1 new file mode 100644 index 0000000..6688fac --- /dev/null +++ b/Send-TwitterDirectMessagev2.ps1 @@ -0,0 +1,201 @@ +function Get-OAuthAuthorization { + <# + .SYNOPSIS + This function is used to setup all the appropriate security stuff needed to issue + API calls against Twitter's API. It has been tested with v1.1 of the API. It currently + includes support only for sending tweets from a single user account and to send DMs from + a single user account. + .EXAMPLE + Get-OAuthAuthorization -DmMessage 'hello' -HttpEndPoint 'https://api.twitter.com/1.1/direct_messages/new.json' -Username adam + + This example gets the authorization string needed in the HTTP POST method to send a direct + message with the text 'hello' to the user 'adam'. + .EXAMPLE + Get-OAuthAuthorization -TweetMessage 'hello' -HttpEndPoint 'https://api.twitter.com/1.1/statuses/update.json' + + This example gets the authorization string needed in the HTTP POST method to send out a tweet. + .PARAMETER HttpEndPoint + This is the URI that you must use to issue calls to the API. + .PARAMETER TweetMessage + Use this parameter if you're sending a tweet. This is the tweet's text. + .PARAMETER DmMessage + If you're sending a DM to someone, this is the DM's text. + .PARAMETER Username + If you're sending a DM to someone, this is the username you'll be sending to. + .PARAMETER ApiKey + The API key for the Twitter application you previously setup. + .PARAMETER ApiSecret + The API secret key for the Twitter application you previously setup. + .PARAMETER AccessToken + The access token that you generated within your Twitter application. + .PARAMETER + The access token secret that you generated within your Twitter application. + #> + [CmdletBinding(DefaultParameterSetName = 'None')] + [OutputType('System.Management.Automation.PSCustomObject')] + param ( + [Parameter(Mandatory)] + [string]$HttpEndPoint, + [Parameter(Mandatory,ParameterSetName='NewTweet')] + [string]$TweetMessage, + [Parameter(Mandatory, ParameterSetName = 'DM')] + [string]$DmMessage, + [Parameter(Mandatory,ParameterSetName='DM')] + [string]$Username, + [Parameter()] + [string]$ApiKey = 'qE0WtEleJzp8ErWI82B3Jw8gl', + [Parameter()] + [string]$ApiSecret = 'r06813yKNEjaErJc4mkacWmFSNwnT1H05Yyjd8XQ5bOhKxVBVc', + [Parameter()] + [string]$AccessToken = '19891458-FL3362GmrreMWUfoPM9Y1gpluAigRbsS36Fv2t2d0', + [Parameter()] + [string]$AccessTokenSecret = 'muMHKAbmdIu1P462gZel4HhhnySGx6VwoaeN2cBVt8EEf' + ) + + begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + try { + [Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null + [Reflection.Assembly]::LoadWithPartialName("System.Net") | Out-Null + } catch { + Write-Error $_.Exception.Message + } + } + + process { + try { + ## Generate a random 32-byte string. I'm using the current time (in seconds) and appending 5 chars to the end to get to 32 bytes + ## Base64 allows for an '=' but Twitter does not. If this is found, replace it with some alphanumeric character + $OauthNonce = [System.Convert]::ToBase64String(([System.Text.Encoding]::ASCII.GetBytes("$([System.DateTime]::Now.Ticks.ToString())12345"))).Replace('=', 'g') + Write-Verbose "Generated Oauth none string '$OauthNonce'" + + ## Find the total seconds since 1/1/1970 (epoch time) + $EpochTimeNow = [System.DateTime]::UtcNow - [System.DateTime]::ParseExact("01/01/1970", "dd/MM/yyyy", $null) + Write-Verbose "Generated epoch time '$EpochTimeNow'" + $OauthTimestamp = [System.Convert]::ToInt64($EpochTimeNow.TotalSeconds).ToString(); + Write-Verbose "Generated Oauth timestamp '$OauthTimestamp'" + + ## Build the signature + $SignatureBase = "$([System.Uri]::EscapeDataString($HttpEndPoint))&" + $SignatureParams = @{ + 'oauth_consumer_key' = $ApiKey; + 'oauth_nonce' = $OauthNonce; + 'oauth_signature_method' = 'HMAC-SHA1'; + 'oauth_timestamp' = $OauthTimestamp; + 'oauth_token' = $AccessToken; + 'oauth_version' = '1.0'; + } + if ($TweetMessage) { + $SignatureParams.status = $TweetMessage + } elseif ($DmMessage) { + $SignatureParams.screen_name = $Username + $SignatureParams.text = $DmMessage + } + + ## Create a string called $SignatureBase that joins all URL encoded 'Key=Value' elements with a & + ## Remove the URL encoded & at the end and prepend the necessary 'POST&' verb to the front + $SignatureParams.GetEnumerator() | sort name | foreach { $SignatureBase += [System.Uri]::EscapeDataString("$($_.Key)=$($_.Value)&") } + $SignatureBase = $SignatureBase.TrimEnd('%26') + $SignatureBase = 'POST&' + $SignatureBase + Write-Verbose "Base signature generated '$SignatureBase'" + + ## Create the hashed string from the base signature + $SignatureKey = [System.Uri]::EscapeDataString($ApiSecret) + "&" + [System.Uri]::EscapeDataString($AccessTokenSecret); + + $hmacsha1 = new-object System.Security.Cryptography.HMACSHA1; + $hmacsha1.Key = [System.Text.Encoding]::ASCII.GetBytes($SignatureKey); + $OauthSignature = [System.Convert]::ToBase64String($hmacsha1.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($SignatureBase))); + Write-Verbose "Using signature '$OauthSignature'" + + ## Build the authorization headers using most of the signature headers elements. This is joining all of the 'Key=Value' elements again + ## and only URL encoding the Values this time while including non-URL encoded double quotes around each value + $AuthorizationParams = $SignatureParams + $AuthorizationParams.Add('oauth_signature', $OauthSignature) + + ## Remove any API call-specific params from the authorization params + $AuthorizationParams.Remove('status') + $AuthorizationParams.Remove('text') + $AuthorizationParams.Remove('screen_name') + + $AuthorizationString = 'OAuth ' + $AuthorizationParams.GetEnumerator() | sort name | foreach { $AuthorizationString += $_.Key + '="' + [System.Uri]::EscapeDataString($_.Value) + '", ' } + $AuthorizationString = $AuthorizationString.TrimEnd(', ') + Write-Verbose "Using authorization string '$AuthorizationString'" + + $AuthorizationString + + } catch { + Write-Error $_.Exception.Message + } + } +} + +function Send-Tweet { + <# + .SYNOPSIS + This sends a tweet under a username. + .EXAMPLE + Send-Tweet -Message 'hello, world' + + This example will send a tweet with the text 'hello, world'. + .PARAMETER Message + The text of the tweet. + #> + [CmdletBinding()] + [OutputType('System.Management.Automation.PSCustomObject')] + param ( + [Parameter(Mandatory)] + [ValidateLength(1, 140)] + [string]$Message + ) + + process { + $HttpEndPoint = 'https://api.twitter.com/1.1/statuses/update.json' + + $AuthorizationString = Get-OAuthAuthorization -TweetMessage $Message -HttpEndPoint $HttpEndPoint + + ## Convert the message to a Byte array + $Body = [System.Text.Encoding]::ASCII.GetBytes("status=$Message"); + Write-Verbose "Using POST body '$Body'" + Invoke-RestMethod -URI $HttpEndPoint -Method Post -Body $Body -Headers @{ 'Authorization' = $AuthorizationString } -ContentType "application/x-www-form-urlencoded" + } +} + +function Send-TwitterDm { + <# + .SYNOPSIS + This sends a DM to another Twitter user. + .EXAMPLE + Send-TwitterDm -Message 'hello, Adam' -Username 'adam' + + This sends a DM with the text 'hello, Adam' to the username 'adam' + .PARAMETER Message + The text of the DM. + .PARAMETER Username + The username you'd like to send the DM to. + #> + [CmdletBinding(DefaultParameterSetName='None')] + [OutputType('System.Management.Automation.PSCustomObject')] + param ( + [Parameter(Mandatory)] + [ValidateLength(1, 140)] + [string]$Message, + [Parameter(Mandatory,ParameterSetName='Username')] + [string]$Username + ) + + process { + $HttpEndPoint = 'https://api.twitter.com/1.1/direct_messages/new.json' + + $AuthorizationString = Get-OAuthAuthorization -DmMessage $Message -HttpEndPoint $HttpEndPoint -Username $Username -Verbose + + ## Convert the message to a Byte array + $Message = [System.Uri]::EscapeDataString($Message) + $Username = [System.Uri]::EscapeDataString($Username) + $Body = [System.Text.Encoding]::ASCII.GetBytes("text=$Message&screen_name=$Username"); + Write-Verbose "Using POST body '$Body'" + Invoke-RestMethod -URI $HttpEndPoint -Method Post -Body $Body -Headers @{ 'Authorization' = $AuthorizationString } -ContentType "application/x-www-form-urlencoded" + + } +} \ No newline at end of file diff --git a/Send-WolProxyRequest.ps1 b/Send-WolProxyRequest.ps1 new file mode 100644 index 0000000..36ac85d --- /dev/null +++ b/Send-WolProxyRequest.ps1 @@ -0,0 +1,462 @@ +#Requires -Version 3 + +<# +.NOTES + Created on: 5/21/2014 1:33 PM + Created by: Adam Bertram + Filename: Send-WolProxyRequest.ps1 + General Requirements: Read access to a System Center Configuration Manager database + Requirements: + Wake On LAN Command Line Utility (http://www.depicus.com/wake-on-lan/wake-on-lan-cmd.aspx) + Todos: + Remove the dependency on wolcmd.exe. Use the Net.Sockets.UdpClient object instead. + Speed this up by using jobs +.DESCRIPTION + This script is designed to send a WOL magic packet to a specified computer. If the specified computer is not + on the same network as the originatnating computer, it will attempt to find a "proxy" Windows computer to + initiate the WOL send. This gets around traditional network multicasting requirements. + + This script currently requires access to a System Center Configuration Manager database. This + script uses it to find the various network information about the specified computer to wake. + + This script uses a text file to store known good candidates to be used as WOL proxies. It can either + be prepopulated with Windows PCs or as you use this script more the script will populate it with + all of the WOL proxies it picks as to speed up the selection process. +.EXAMPLE + .\Send-WolProxyRequest.ps1 -Computername COMPUTERNAME +.EXAMPLE + .\Send-WolProxyRequest.ps1 -Computername COMPUTERNAME -UsePsRemoting +.PARAMETER Computername + This computer name that you'd like to attempt to wake up. +.PARAMETER ConfigMgrSite + The site code of your ConfigMgr site +.PARAMETER ConfigMgrSiteServer + The computer name of your ConfigMgr site server hosting your database +.PARAMETER WolCmdFilePath + The file path where the wolcmd.exe utility is located +.PARAMETER UsePsRemoting + Use this switch if you'd like to use Powershell remoting to kick off the WOL attempt on the WOL proxy + rather than using WMI to initiate the remote process +.PARAMETER KnownGoodWolProxyHostsFilePath + The file path to the text file that contains any Windows computers that were previously + found to be suitable WOL proxies. +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory = $True, + ValueFromPipeline = $True, + ValueFromPipelineByPropertyName = $True)] + [string]$Computername, + [Parameter(Mandatory = $False, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $False)] + [string]$ConfigMgrSite = 'UHP', + [Parameter(Mandatory = $False, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $False)] + [ValidateScript({ Test-Connection $_ -Quiet -Count 1 })] + [string]$ConfigMgrSiteServer = 'CONFIGMANAGER', + [Parameter(Mandatory = $False, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $False)] + [ValidateScript({ Test-Path $_ })] + [string]$WolCmdFilePath = '\\hosp.uhhg.org\netlogon\wolcmd.exe', + [Parameter(Mandatory = $False, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $False)] + [switch]$UsePsRemoting = $false, + [Parameter(Mandatory = $False, + ValueFromPipeline = $False, + ValueFromPipelineByPropertyName = $False)] + [string]$KnownGoodWolProxyHostsFilePath = "$($env:USERPROFILE)\desktop\KnownGoodWolProxies.txt" +) + +begin { + + function ConvertTo-DecimalIP { + <# + .Synopsis + Converts a Decimal IP address into a 32-bit unsigned integer. + .Description + ConvertTo-DecimalIP takes a decimal IP, uses a shift-like operation on each octet and returns a single UInt32 value. + http://www.indented.co.uk/2010/01/23/powershell-subnet-math/ + .Parameter IPAddress + An IP Address to convert. + #> + + [CmdLetBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [Net.IPAddress]$IPAddress + ) + + process { + $i = 3; $DecimalIP = 0; + $IPAddress.GetAddressBytes() | ForEach-Object { $DecimalIP += $_ * [Math]::Pow(256, $i); $i-- } + + return [UInt32]$DecimalIP + } + } + + function ConvertTo-DottedDecimalIP { + <# + .Synopsis + Returns a dotted decimal IP address from either an unsigned 32-bit integer or a dotted binary string. + .Description + ConvertTo-DottedDecimalIP uses a regular expression match on the input string to convert to an IP address. + http://www.indented.co.uk/2010/01/23/powershell-subnet-math/ + .Parameter IPAddress + A string representation of an IP address from either UInt32 or dotted binary. + #> + + [CmdLetBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [String]$IPAddress + ) + + process { + Switch -RegEx ($IPAddress) { + "([01]{8}.){3}[01]{8}" { + return [String]::Join('.', $($IPAddress.Split('.') | ForEach-Object { [Convert]::ToUInt32($_, 2) })) + } + "\d" { + $IPAddress = [UInt32]$IPAddress + $DottedIP = $(For ($i = 3; $i -gt -1; $i--) { + $Remainder = $IPAddress % [Math]::Pow(256, $i) + ($IPAddress - $Remainder) / [Math]::Pow(256, $i) + $IPAddress = $Remainder + }) + + return [String]::Join('.', $DottedIP) + } + default { + Write-Error "Cannot convert this format" + } + } + } + } + + function Get-NetworkAddress { + <# + .Synopsis + Takes an IP address and subnet mask then calculates the network address for the range. + .Description + Get-NetworkAddress returns the network address for a subnet by performing a bitwise AND + operation against the decimal forms of the IP address and subnet mask. Get-NetworkAddress + expects both the IP address and subnet mask in dotted decimal format. + http://www.indented.co.uk/2010/01/23/powershell-subnet-math/ + .Parameter IPAddress + Any IP address within the network range. + .Parameter SubnetMask + The subnet mask for the network. + #> + + [CmdLetBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [Net.IPAddress]$IPAddress, + + [Parameter(Mandatory = $true, Position = 1)] + [Alias("Mask")] + [Net.IPAddress]$SubnetMask + ) + + process { + [pscustomobject]@{ 'NetworkAddress' = (ConvertTo-DottedDecimalIP ((ConvertTo-DecimalIP $IPAddress) -band (ConvertTo-DecimalIP $SubnetMask))) } + } + } + + function ConvertTo-Mask { + <# + .Synopsis + Returns a dotted decimal subnet mask from a mask length. + .Description + ConvertTo-Mask returns a subnet mask in dotted decimal format from an integer value ranging + between 0 and 32. ConvertTo-Mask first creates a binary string from the length, converts + that to an unsigned 32-bit integer then calls ConvertTo-DottedDecimalIP to complete the operation. + .Parameter MaskLength + The number of bits which must be masked. + #> + + [CmdLetBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [Alias("Length")] + [ValidateRange(0, 32)] + $MaskLength + ) + + Process { + return ConvertTo-DottedDecimalIP ([Convert]::ToUInt32($(("1" * $MaskLength).PadRight(32, "0")), 2)) + } + } + + function Get-NetworkRange([String]$IP, [String]$Mask) { + if ($IP.Contains("/")) { + $Temp = $IP.Split("/") + $IP = $Temp[0] + $Mask = $Temp[1] + } + + if (!$Mask.Contains(".")) { + $Mask = ConvertTo-Mask $Mask + } + + $DecimalIP = ConvertTo-DecimalIP $IP + $DecimalMask = ConvertTo-DecimalIP $Mask + + $Network = $DecimalIP -band $DecimalMask + $Broadcast = $DecimalIP -bor ((-bnot $DecimalMask) -band [UInt32]::MaxValue) + + for ($i = $($Network + 1); $i -lt $Broadcast; $i++) { + ConvertTo-DottedDecimalIP $i + } + } + + function Get-OfflineComputerNetworkInformation ($Computername) { + $WmiQuery = "SELECT DISTINCT * + FROM SMS_R_System AS sys + JOIN SMS_G_System_NETWORK_ADAPTER_CONFIGURATION AS net ON net.ResourceID = sys.ResourceID + WHERE sys.Name = '$ComputerName' AND + net.IPAddress IS NOT NULL" + + $WmiParams = @{ + 'ComputerName' = $ConfigMgrSiteServer + 'Namespace' = "root\sms\site_$ConfigMgrSite" + 'Query' = $WmiQuery + } + + ## Query all network interfaces on the local machine and parse out IP address, subnet mask and the MAC + Write-Verbose "Querying site server '$ConfigMgrSiteServer' with query '$WmiQuery'" + try { + $Output = @{ } + $NetworkInfo = Get-WmiObject @WmiParams + if (!$NetworkInfo) { + throw "Computer '$Computername' could not be found in the SCCM database" + } else { + $NetworkInfo | foreach { + $Output.IPAddress = [string]([regex]'\b(?:\d{1,3}\.){3}\d{1,3}\b').Matches($_.net.IPAddress) + $Output.SubnetMask = [string]([regex]'\b(?:\d{1,3}\.){3}\d{1,3}\b').Matches($_.net.IPSubnet) + $Output.MACAddress = [string](($_.net.MACAddress.replace(":", "")).replace("-", "")).replace(".", "") + } + } + [pscustomobject]$Output + } catch { + Write-Error $_.Exception.Message + } + } + + function Get-LocalIpNetwork { + Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter "IPEnabled = 'True'" | where { $_.IPAddress -and $_.IPSubnet } | foreach { + [pscustomobject]@{ 'LocalIPNetwork' = (Get-NetworkAddress -IPAddress $_.IPAddress[0] -SubnetMask $_.IPSubnet[0]) } + } + } + + function Test-Ping { + param ($ComputerName) + try { + $oPing = new-object system.net.networkinformation.ping; + if (($oPing.Send($ComputerName, 200).Status -eq 'TimedOut')) { + $false + } else { + $true + } + } catch [System.Exception] { + $false + }##endtry + } + + function Test-Wmi ($IpAddress) { + try { + $Result = ([WMICLASS]"\\$IpAddress\Root\CIMV2:Win32_Process").create("hostname") + if ($Result.ReturnValue -eq 0) { + $true + } else { + $false + } + } catch { + $false + } + } + + function Validate-IsValidHost ($IpAddress) { + try { + Write-Verbose "Testing $IpAddress Ping" + if (Test-Ping -ComputerName $IpAddress) { + Write-Verbose "Testing $IpAddress Ping - Success" + ## Assume if the C$ share is available, it's a Windows computer we can + ## copy the wolcmd utility to and run. This could be better. + Write-Verbose "Testing $IpAddress SMB share file copy and removal" + ## Create a temp text file and try to copy it over to test access + $TestFilePath = "$($env:SystemDrive)\testcopy.txt" + Add-Content -Path $TestFilePath -Value '' -Force + Copy-Item -Path $TestFilePath -Destination "\\$IpAddress\c$" -Force + Remove-Item -Path "\\$IpAddress\c$\testcopy.txt" -Force + ## If it hasn't thrown an error yet then we've confirmed we can copy and delete a file from it + Write-Verbose "Testing $IpAddress SMB share file copy and removal - Success" + Write-Verbose "Testing $IpAddress remote WMI process creation" + if (Test-Wmi -IpAddress $IpAddress) { + Write-Verbose "Testing $IpAddress remote WMI process creation - Success" + Write-Verbose "All tests passed. $IpAddress is a good WOL proxy" + $true + } else { + throw 'Remote process could not be created with WMI' + } + } else { + throw 'Host is offline' + } + } catch { + Write-Warning "Test failed with error '$($_.Exception.Message)' for $IpAddress" + $false + } + } + + function Get-WolProxy ($IpAddress, $SubnetMask) { + ## Check if any known good WOL proxy exists before scanning all the IPs + ## in that network + $IpNetwork = Get-NetworkAddress -IPAddress $IpAddress -SubnetMask $SubnetMask + $KnownGoodWolProxy = Get-KnownGoodWolProxy -IpNetwork $IpNetwork.NetworkAddress + if ($KnownGoodWolProxy) { + return $KnownGoodWolProxy + } else { + $HostIps = Get-NetworkRange -IP $IpAddress -Mask $SubnetMask + foreach ($Ip in $HostIps) { + Write-Verbose "Checking $Ip if good candidate for WOL proxy..." + ## Check to see if our WOL proxy PC is online + if (Validate-IsValidHost -IpAddress $Ip) { + Write-Verbose "WOL Proxy found: $Ip" + + ## Add this computer to the known good proxy list for later + New-KnownGoodWolProxy -HostProxy @{ 'IpAddress' = $Ip; 'SubnetMask' = $SubnetMask } + + return $Ip + } else { + Write-Verbose "IP address '$Ip' will not work as a WOL proxy" + } + } + $false + } + } + + ## WOL proxy Windows hosts that are online and accessible + function New-KnownGoodWolProxy ([hashtable]$HostProxy) { + if (!(Get-KnownGoodWolProxy $HostProxy.IpAddress)) { + Write-Verbose "The '$($HostProxy.IpAddress)' host is not yet in the known good WOL proxy host file." + ## Create IP network from IP address and subnet mask + $IpNetwork = Get-NetworkAddress -IPAddress $HostProxy.IpAddress -SubnetMask $HostProxy.SubnetMask + Write-Verbose "Adding IP Address: $($HostProxy.IpAddress) IP Network: $IpNetwork SubnetMask: $($HostProxy.SubnetMask) to known good WOL proxy host file" + [pscustomobject]@{ + 'IpAddress' = $HostProxy.IpAddress; + 'IpNetwork' = $IpNetwork.NetworkAddress; + 'SubnetMask' = $HostProxy.SubnetMask + } | Export-Csv -Path $KnownGoodWolProxyHostsFilePath -Append -NoTypeInformation + } + + } + + function Remove-KnownGoodWolProxy ($IpAddress) { + $NewCsvContents = Import-Csv -Path $KnownGoodWolProxyHostsFilePath | where { $_.IpAddress -ne $IpAddress } + ##TODO: This removes the row and headers if only 1 row exists + $NewCsvContents | Export-Csv -Path $KnownGoodWolProxyHostsFilePath -NoTypeInformation + } + + ## Searches the known good WOL proxy file for an accessible host in the IP network specified + function Get-KnownGoodWolProxy ([string]$IpNetwork) { + if (!(Test-Path $KnownGoodWolProxyHostsFilePath)) { + Write-Verbose "Known good WOL proxy host file at '$KnownGoodWolProxyHostsFilePath' does not exist" + $false + } else { + $Hosts = Import-Csv -Path $KnownGoodWolProxyHostsFilePath | where { $_.IpNetwork -eq $IpNetwork } + if (!$Hosts) { + Write-Verbose "No known good WOL proxy hosts found in IP network '$IpNetwork'" + $false + } else { + $HostsOnNet = $Hosts | where { $_.IpNetwork -eq $IpNetwork } + if ($HostsOnNet) { + Write-Verbose "$(($HostsOnNet | measure -Sum -ea silentlycontinue).Count) (unknown accessibility) known good WOL proxy hosts found on IP network '$IpNetwork'" + Write-Verbose 'Checking previously known good WOL proxy hosts if still usable' + $AccessibleHost = $HostsOnNet | where { Validate-IsValidHost $_.IpAddress } | select -First 1 -ExpandProperty IpAddress + if ($AccessibleHost) { + Write-Verbose "Found hostname '$AccessibleHost' still to be a good WOL proxy host" + $AccessibleHost + } else { + Remove-KnownGoodWolProxy -IpAddress $_.IpAddress + } + } else { + Write-Verbose "No accessible, known good WOL proxy hosts on the '$IpNetwork' found" + } + } + } + } + + function Send-WolPacketLocally ($MacAddress, $IpNetwork, $SubnetMask) { + & $WolCmdFilePath $MacAddress $IPNetwork $SubnetMask $WolUdpPort 2>&1> $null + } + + function Invoke-WolProxy ($IpAddress, $OfflineMacAddress, $OfflineIpNetwork, $OfflineSubnetMask) { + ## Remove the dependency on wolcmd.exe. Use the Net.Sockets.UdpClient object instead. + ## Copy wolcmd to the remote proxy computer + Write-Verbose "Copying $WolCmdFilePath to \\$IpAddress\c`$..." + Copy-Item $WolCmdFilePath "\\$IpAddress\c$" -Force + + $WolCmdString = "C:\$($WolCmdFilePath | Split-Path -Leaf) $OfflineMacAddress $OfflineIPNetwork $OfflineSubnetMask $WolUdpPort" + Write-Verbose "Initiating the string `"$WolCmdString`"..." + Write-Verbose "Connecting to $IpAddress and attempting WOL proxy function via WMI RPC method..." + $Result = ([WMICLASS]"\\$IpAddress\Root\CIMV2:Win32_Process").create($WolCmdString) + if ($Result) { + Write-Verbose "Waiting for process ID $($Result.ProcessID) on IP $IpAddress..." + while (Get-Process -Id $Result.ProcessID -ComputerName $IpAddress -ErrorAction 'SilentlyContinue') { + sleep 1 + } + Write-Verbose "Process ID $($Result.ProcessID) has exited" + } else { + Write-Warning "Failed to initiate WMI process creation on '$IpAddress'. Exit code was '$($NewProcess.ReturnValue)'" + } + #} + ## Cleanup all files copied to the proxy computer + Write-Verbose 'Cleaning up file remnants on WOL proxy computer...' + if (Test-Path "\\$IpAddress\c`$\$($WolCmdFilePath | Split-Path -Leaf)") { + Remove-Item -Path "\\$IpAddress\c`$\$($WolCmdFilePath | Split-Path -Leaf)" -Force + } + } + + if (Test-Connection -ComputerName $Computername -Quiet -Count 1) { + Write-Verbose -Message "The computer $Computername is already online" + return + } + + ## Find all of the IP/subnet masks on the local computer + $LocalIPAddressNetworks = Get-LocalIpNetwork + Write-Verbose "Found $($LocalIPAddressNetworks.Count) local IP networks" + + ## Common WOL UDP ports are 7 and 9 + $WolUdpPort = 9 + + $OfflineComputerNetwork = Get-OfflineComputerNetworkInformation $Computername + +} +process { + try { + ## TODO: Make this run in parallel by jobs of a foreach -parallel workflow loop + foreach ($Network in $OfflineComputerNetwork) { + Write-Verbose "Processing IP address $($Network.IPAddress)..." + Write-Verbose "Checking the remote network to see if it's on any local IP network..." + $RemoteIpNetwork = Get-NetworkAddress -IPAddress $Network.IpAddress -SubnetMask $Network.SubnetMask + if ($LocalIPNetworks.LocalIPNetwork -contains $RemoteIpNetwork.NetworkAddress) { + Write-Verbose 'IP found to be on local subnet. No WOL proxy needed. Sending WOL directly to the intended machine...' + Send-WolPacketLocally -MacAddress $Network.MacAddress -IpNetwork $RemoteIpNetwork.NetworkAddress -SubnetMask $Network.SubnetMask + } else { + Write-Verbose 'IP not found to be on local subnet. Getting WOL proxy computer...' + $WolProxy = Get-WolProxy -IpAddress $Network.IPAddress -SubnetMask $Network.SubnetMask + if (!$WolProxy) { + Write-Warning "Unable to find a WOL proxy for '$Computername'" + } else { + Invoke-WolProxy -OfflineIpNetwork $RemoteIpNetwork.NetworkAddress -OfflineMacAddress $Network.MacAddress -OfflineSubnetMask $Network.SubnetMask -IpAddress $WolProxy + } + } + } + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Set-Exchange2013AdSchema.ps1 b/Set-Exchange2013AdSchema.ps1 new file mode 100644 index 0000000..7790177 --- /dev/null +++ b/Set-Exchange2013AdSchema.ps1 @@ -0,0 +1,119 @@ +#Requires -Module ActiveDirectory + +<# +.SYNOPSIS + This script is designed to expedite the AD schema extension for Exchange 2013. It is an automated way to not only + perform the schema change itself but also to confirm success as well. This script is intended to be run on a domain-joined + workstation under an account that is both in the Schema Admins group and the Enterprise Admins group. + + It will require the Exhcange 2013 media (eval or licensed) +.NOTES + Created on: 8/22/2014 + Created by: Adam Bertram + Filename: Set-Exchange2013AdSchema.ps1 +.EXAMPLE + +.EXAMPLE + +.PARAMETER ExchangeMediaFolderPath + The path to where the contents of the Exchange 2013 media is located. This is typically the ISO extracted. + +#> +[CmdletBinding()] +[OutputType([bool])] +param ( + [Parameter(Mandatory)] + [ValidateScript({Test-Path $_ -PathType 'Container'})] + [string]$ExchangeMediaFolderPath +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + + function Test-GroupMembership { + $RequiredGroups = 'Schema Admins', 'Enterprise Admins' + $Username = whoami + $Username = $Username.Split('\')[1] + $Groups = (Get-ADUser -Identity $Username -Properties Memberof).MemberOf | foreach { $_.Split(',')[0].TrimStart('CN=') } + if (($Groups | where { $RequiredGroups -contains $_ }).Count -ne 2) { + $false + } else { + $true + } + } + + function Get-InstalledSoftwareInRegistry { + <# + .SYNOPSIS + Retrieves a list of all software installed + .DESCRIPTION + Retrieves a list of all software installed via the specified method + .EXAMPLE + Get-InstalledSoftware + This example retrieves all software installed on the local computer + .PARAMETER Computername + Use this parameter if you'd like to query installed software on a remote computer + #> + [CmdletBinding()] + param ( + [string]$Computername = 'localhost' + ) + process { + try { + $ScriptBlock = { + $UninstallKeys = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" + New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS | Out-Null + $UninstallKeys += Get-ChildItem HKU: | where { $_.Name -match 'S-\d-\d+-(\d+-){1,14}\d+$' } | foreach { "HKU:\$($_.PSChildName)\Software\Microsoft\Windows\CurrentVersion\Uninstall" } + foreach ($UninstallKey in $UninstallKeys) { + $Keys = Get-ItemProperty -Path "$UninstallKey\*" -ErrorAction SilentlyContinue + foreach ($Key in ($Keys | where { $_.SystemComponent -ne '1' })) { + $Key + } + } + } + if ($Computername -ne 'localhost') { + Invoke-Command -ComputerName $Computername -ScriptBlock $ScriptBlock + } else { + & $ScriptBlock + } + } catch { + $_.Exception.Message + } + } + } +} + +process { + try { + ## Ensure the account this is being run under is in the appropriate groups + if (-not (Test-GroupMembership)) { + throw "The user is not in the proper groups" + } + + ## Ensure .NET 4.5 and at least PSv3 is installed on the schema master + $SchemaMaster = (Get-ADForest).SchemaMaster + + ## Disable replication on the schema master to prevent any potential problems from replicating out + + ## Perform the schema extension + + ## Verify the extension was successful by checking the log file + + ## Verify the extension was successful by checking for the msExch attribute on the user + + ## Reenable replication + + ## Ensure replication is successful + } catch { + Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + } +} + +end { + try { + + } catch { + Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + } +} \ No newline at end of file diff --git a/Set-MsRandomPasswordToLocalUserAccount.ps1 b/Set-MsRandomPasswordToLocalUserAccount.ps1 new file mode 100644 index 0000000..b050fbe --- /dev/null +++ b/Set-MsRandomPasswordToLocalUserAccount.ps1 @@ -0,0 +1,267 @@ +function Invoke-PasswordRoll { + <# + .SYNOPSIS + + This script can be used to set the local account passwords on remote machines to random passwords. The username/password/server combination will be saved in a CSV file. + The account passwords stored in the CSV file can be encrypted using a password of the administrators choosing to ensure clear-text account passwords aren't written to disk. + The encrypted passwords can be decrypted using another function in this file: ConvertTo-CleartextPassword + + + Function: Invoke-PasswordRoll + Author: Microsoft + Version: 1.0 + + .DESCRIPTION + + This script can be used to set the local account passwords on remote machines to random passwords. The username/password/server combination will be saved in a CSV file. + The account passwords stored in the CSV file can be encrypted using a password of the administrators choosing to ensure clear-text account passwords aren't written to disk. + The encrypted passwords can be decrypted using another function in this file: ConvertTo-CleartextPassword + + .PARAMETER ComputerName + + An array of computers to run the script against using PowerShell remoting. + + .PARAMETER LocalAccounts + + An array of local accounts whose password should be changed. + + .PARAMETER TsvFileName + + The file to output the username/password/server combinations to. + + .PARAMETER EncryptionKey + + A password to encrypt the TSV file with. Uses AES encryption. Only the passwords stored in the TSV file will be encrypted, the username and servername will be clear-text. + + .PARAMETER PasswordLength + + The length of the passwords which will be randomly generated for local accounts. + + .PARAMETER NoEncryption + + Do not encrypt the account passwords stored in the TSV file. This will result in clear-text passwords being written to disk. + + .EXAMPLE + + . .\Invoke-PasswordRoll.ps1 #Loads the functions in this script file + Invoke-PasswordRoll -ComputerName (Get-Content computerlist.txt) -LocalAccounts @("administrator","CustomLocalAdmin") -TsvFileName "LocalAdminCredentials.tsv" -EncryptionKey "Password1" + + Connects to all the computers stored in the file "computerlist.txt". If the local account "administrator" and/or "CustomLocalAdmin" are present on the system, their password is changed + to a randomly generated password of length 20 (the default). The username/password/server combinations are stored in LocalAdminCredentials.tsv, and the account passwords are AES encrypted using the password "Password1". + + .EXAMPLE + + . .\Invoke-PasswordRoll.ps1 #Loads the functions in this script file + Invoke-PasswordRoll -ComputerName (Get-Content computerlist.txt) -LocalAccounts @("administrator") -TsvFileName "LocalAdminCredentials.tsv" -NoEncryption -PasswordLength 40 + + Connects to all the computers stored in the file "computerlist.txt". If the local account "administrator" is present on the system, its password is changed to a random generated + password of length 40. The username/password/server combinations are stored in LocalAdminCredentials.tsv unencrypted. + + .NOTES + Requirements: + -PowerShellv2 or above must be installed + -PowerShell remoting must be enabled on all systems the script will be run against + + Script behavior: + -If a local account is present on the system, but not specified in the LocalAccounts parameter, the script will write a warning to the screen to alert you to the presence of this local account. The script will continue running when this happens. + -If a local account is specified in the LocalAccounts parameter, but the account does not exist on the computer, nothing will happen (an account will NOT be created). + -The function ConvertTo-CleartextPassword, contained in this file, can be used to decrypt passwords that are stored encrypted in the TSV file. + -If a server specified in ComputerName cannot be connected to, PowerShell will output an error message. + -Microsoft advises companies to regularly roll all local and domain account passwords. + + #> + [CmdletBinding(DefaultParameterSetName = "Encryption")] + Param ( + [Parameter(Mandatory = $true)] + [String[]] + $ComputerName, + + [Parameter(Mandatory = $true)] + [String[]] + $LocalAccounts, + + [Parameter(Mandatory = $true)] + [String] + $TsvFileName, + + [Parameter(ParameterSetName = "Encryption", Mandatory = $true)] + [String] + $EncryptionKey, + + [Parameter()] + [ValidateRange(20, 120)] + [Int] + $PasswordLength = 20, + + [Parameter(ParameterSetName = "NoEncryption", Mandatory = $true)] + [Switch] + $NoEncryption + ) + + + #Load any needed .net classes + Add-Type -AssemblyName "System.Web" -ErrorAction Stop + + + #This is the scriptblock that will be executed on every computer specified in ComputerName + $RemoteRollScript = { + Param ( + [Parameter(Mandatory = $true, Position = 1)] + [String[]] + $Passwords, + + [Parameter(Mandatory = $true, Position = 2)] + [String[]] + $LocalAccounts, + + #This is here so I can record what the server name that the script connected to was, sometimes the DNS records get messed up, it can be nice to have this. + [Parameter(Mandatory = $true, Position = 3)] + [String] + $TargettedServerName + ) + + $LocalUsers = Get-WmiObject Win32_UserAccount -Filter "LocalAccount=true" | Foreach { $_.Name } + + #Check if the computer has any local user accounts whose passwords are not going to be rolled by this script + foreach ($User in $LocalUsers) { + if ($LocalAccounts -inotcontains $User) { + Write-Warning "Server: '$($TargettedServerName)' has a local account '$($User)' whos password is NOT being changed by this script" + } + } + + #For every local account specified that exists on this server, change the password + $PasswordIndex = 0 + foreach ($LocalAdmin in $LocalAccounts) { + $Password = $Passwords[$PasswordIndex] + + if ($LocalUsers -icontains $LocalAdmin) { + try { + $objUser = [ADSI]"WinNT://localhost/$($LocalAdmin), user" + $objUser.psbase.Invoke("SetPassword", $Password) + + $Properties = @{ + TargettedServerName = $TargettedServerName + Username = $LocalAdmin + Password = $Password + RealServerName = $env:computername + } + + $ReturnData = New-Object PSObject -Property $Properties + Write-Output $ReturnData + } catch { + Write-Error "Error changing password for user:$($LocalAdmin) on server:$($TargettedServerName)" + } + } + + $PasswordIndex++ + } + } + + + #Generate the password on the client running this script, not on the remote machine. System.Web.Security isn't available in the .NET Client profile. Making this call + # on the client running the script ensures only 1 computer needs the full .NET runtime installed (as opposed to every system having the password rolled). + function Create-RandomPassword { + Param ( + [Parameter(Mandatory = $true)] + [ValidateRange(20, 120)] + [Int] + $PasswordLength + ) + + $Password = [System.Web.Security.Membership]::GeneratePassword($PasswordLength, $PasswordLength / 4) + + #This should never fail, but I'm putting a sanity check here anyways + if ($Password.Length -ne $PasswordLength) { + throw new Exception("Password returned by GeneratePassword is not the same length as required. Required length: $($PasswordLength). Generated length: $($Password.Length)") + } + + return $Password + } + + + #Main functionality - Generate a password and remote in to machines to change the password of local accounts specified + if ($PsCmdlet.ParameterSetName -ieq "Encryption") { + try { + $Sha256 = new-object System.Security.Cryptography.SHA256CryptoServiceProvider + $SecureStringKey = $Sha256.ComputeHash([System.Text.UnicodeEncoding]::Unicode.GetBytes($EncryptionKey)) + } catch { + Write-Error "Error creating TSV encryption key" -ErrorAction Stop + } + } + + foreach ($Computer in $ComputerName) { + #Need to generate 1 password for each account that could be changed + $Passwords = @() + for ($i = 0; $i -lt $LocalAccounts.Length; $i++) { + $Passwords += Create-RandomPassword -PasswordLength $PasswordLength + } + + Write-Output "Connecting to server '$($Computer)' to roll specified local admin passwords" + $Result = Invoke-Command -ScriptBlock $RemoteRollScript -ArgumentList @($Passwords, $LocalAccounts, $Computer) -ComputerName $Computer + #If encryption is being used, encrypt the password with the user supplied key prior to writing to disk + if ($Result -ne $null) { + if ($PsCmdlet.ParameterSetName -ieq "NoEncryption") { + $Result | Select-Object Username, Password, TargettedServerName, RealServerName | Export-Csv -Append -Path $TsvFileName -NoTypeInformation + } else { + #Filters out $null entries returned + $Result = $Result | Select-Object Username, Password, TargettedServerName, RealServerName + + foreach ($Record in $Result) { + $PasswordSecureString = ConvertTo-SecureString -AsPlainText -Force -String ($Record.Password) + $Record | Add-Member -MemberType NoteProperty -Name EncryptedPassword -Value (ConvertFrom-SecureString -Key $SecureStringKey -SecureString $PasswordSecureString) + $Record.PSObject.Properties.Remove("Password") + $Record | Select-Object Username, EncryptedPassword, TargettedServerName, RealServerName | Export-Csv -Append -Path $TsvFileName -NoTypeInformation + } + } + } + } +} + +function ConvertTo-CleartextPassword { + <# + .SYNOPSIS + This function can be used to decrypt passwords that were stored encrypted by the function Invoke-PasswordRoll. + + Function: ConvertTo-CleartextPassword + Author: Microsoft + Version: 1.0 + + .DESCRIPTION + This function can be used to decrypt passwords that were stored encrypted by the function Invoke-PasswordRoll. + + + .PARAMETER EncryptedPassword + + The encrypted password that was stored in a TSV file. + + .PARAMETER EncryptionKey + + The password used to do the encryption. + + + .EXAMPLE + + . .\Invoke-PasswordRoll.ps1 #Loads the functions in this script file + ConvertTo-CleartextPassword -EncryptionKey "Password1" -EncryptedPassword 76492d1116743f0423413b16050a5345MgB8AGcAZgBaAHUAaQBwADAAQgB2AGgAcABNADMASwBaAFoAQQBzADEAeABjAEEAPQA9AHwAZgBiAGYAMAA1ADYANgA2ADEANwBkADQAZgAwADMANABjAGUAZQAxAGIAMABiADkANgBiADkAMAA4ADcANwBhADMAYQA3AGYAOABkADcAMQA5ADQAMwBmAGYANQBhADEAYQBjADcANABkADIANgBhADUANwBlADgAMAAyADQANgA1ADIAOQA0AGMAZQA0ADEAMwAzADcANQAyADUANAAzADYAMAA1AGEANgAzADEAMQA5ADAAYwBmADQAZAA2AGQA" + + Decrypts the encrypted password which was stored in the TSV file. + + #> + Param ( + [Parameter(Mandatory = $true)] + [String] + $EncryptedPassword, + + [Parameter(Mandatory = $true)] + [String] + $EncryptionKey + ) + + $Sha256 = new-object System.Security.Cryptography.SHA256CryptoServiceProvider + $SecureStringKey = $Sha256.ComputeHash([System.Text.UnicodeEncoding]::Unicode.GetBytes($EncryptionKey)) + + [SecureString]$SecureStringPassword = ConvertTo-SecureString -String $EncryptedPassword -Key $SecureStringKey + Write-Output ([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($SecureStringPassword))) +} + +Invoke-PasswordRoll -ComputerName a-w7x86-1 -LocalAccounts 'aidet' -NoEncryption -TsvFileName 'file.tsv' \ No newline at end of file diff --git a/Set-RandomPasswordToLocalUserAccount.ps1 b/Set-RandomPasswordToLocalUserAccount.ps1 new file mode 100644 index 0000000..8ce9a42 --- /dev/null +++ b/Set-RandomPasswordToLocalUserAccount.ps1 @@ -0,0 +1,195 @@ +#Requires -Version 3 + +<# +.SYNOPSIS + This finds a local or remote computer(s)' local administrator account and changes it to a random password. +.DESCRIPTION + This finds a local or remote computer(s)' local administrator account, generates a random password, + changes the local administrator password to that random password then records the password into an output file. +.NOTES + Created on: 7/19/2014 + Created by: Adam Bertram + Filename: Set-RandomPasswordToLocalUserAccount.ps1 + Credits: http://support.microsoft.com/kb/2962486 + Todos: Add multiple user support +.EXAMPLE + .\SetRandomPasswordToLocalUserAccount.ps1 -ComputerName 'COMPUTER1','COMPUTER2' -PasswordFilePath 'C:\passwords.txt' + + This sets the local administrator account's password on COMPUTER1 and COMPUTER2 to a randomly generated password + and stores the history in a file called passwords.txt +.EXAMPLE + .\SetRandomPasswordToLocalUserAccount.ps1 -ComputerName 'COMPUTER1','COMPUTER2' -PasswordFilePath 'C:\passwords.txt' -EncryptionKey 'key' + + This sets the local administrator account's password on COMPUTER1 and COMPUTER2 to a randomly generated password + and stores encrypted passwords in a file called passwords.txt +.EXAMPLE + .\SetRandomPasswordToLocalUserAccount.ps1 -ComputerName 'COMPUTER1','COMPUTER2' -PasswordFilePath 'C:\passwords.txt' -EncryptionKey 'key' -PasswordLength 100 + + This sets the local administrator account's password on COMPUTER1 and COMPUTER2 to a randomly generated 100 character password + and stores encrypted passwords in a file called passwords.txt +.PARAMETER Computername + One or more computer names you'd like to change the local administrator password on. If no computer name is selected + then change the local administrator password on the local computer. +.PARAMETER PasswordLength + The length of the password the new local adminsitrator password will be +.PARAMETER PasswordFilePath + The file path to the output file where your passwords are stored +.PARAMETER EncryptionKey + The encryption key you'd like applied to all password strings being being written to the password file +.PARAMETER + If the administrator account is disabled, use this parameter to enable it. If this param is not used and the administrator + account is disabled, it will be skipped. +#> +[CmdletBinding()] + param ( + [Parameter(ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [ValidateScript({ Test-Connection $_ -Quiet -Count 1 })] + [string[]]$Computername = 'localhost', + [Parameter()] + [ValidateRange(20, 120)] + [int]$PasswordLength = 50, + [Parameter(Mandatory)] + [string]$PasswordFilePath, + [Parameter()] + [string]$EncryptionKey, + [Parameter()] + [switch]$EnableAccount + ) + +begin { + function Create-RandomPassword { + <# + .NOTES + Author: Microsoft + #> + Param ( + [Parameter(Mandatory = $true)] + [ValidateRange(20, 120)] + [Int]$PasswordLength + ) + + $Password = [System.Web.Security.Membership]::GeneratePassword($PasswordLength, $PasswordLength / 4) + + #This should never fail, but I'm putting a sanity check here anyways + if ($Password.Length -ne $PasswordLength) { + throw new Exception("Password returned by GeneratePassword is not the same length as required. Required length: $($PasswordLength). Generated length: $($Password.Length)") + } + + return $Password + } + + function Set-Encryption ($UnencryptedPassword, $EncryptionKey) { + try { + $PasswordSecureString = ConvertTo-SecureString -AsPlainText -Force -String $UnencryptedPassword + + $Sha256 = new-object System.Security.Cryptography.SHA256CryptoServiceProvider + $SecureString = $Sha256.ComputeHash([System.Text.UnicodeEncoding]::Unicode.GetBytes($EncryptionKey)) + + ConvertFrom-SecureString -Key $SecureString -SecureString $PasswordSecureString + } catch { + Write-Error "Error creating encryption key" -ErrorAction Stop + $_.Exception.Message + } + } + + function ConvertTo-CleartextPassword { + <# + .SYNOPSIS + This function can be used to decrypt passwords that were stored encrypted by the function Invoke-PasswordRoll. + .NOTES + Author: Microsoft + Version: 1.0 + .DESCRIPTION + This function can be used to decrypt passwords that were stored encrypted by the function Invoke-PasswordRoll. + .PARAMETER EncryptedPassword + The encrypted password that was stored in a TSV file. + .PARAMETER EncryptionKey + The password used to do the encryption. + .EXAMPLE + . .\Invoke-PasswordRoll.ps1 #Loads the functions in this script file + ConvertTo-CleartextPassword -EncryptionKey "Password1" -EncryptedPassword 76492d1116743f0423413b16050a5345MgB8AGcAZgBaAHUAaQBwADAAQgB2AGgAcABNADMASwBaAFoAQQBzADEAeABjAEEAPQA9AHwAZgBiAGYAMAA1ADYANgA2ADEANwBkADQAZgAwADMANABjAGUAZQAxAGIAMABiADkANgBiADkAMAA4ADcANwBhADMAYQA3AGYAOABkADcAMQA5ADQAMwBmAGYANQBhADEAYQBjADcANABkADIANgBhADUANwBlADgAMAAyADQANgA1ADIAOQA0AGMAZQA0ADEAMwAzADcANQAyADUANAAzADYAMAA1AGEANgAzADEAMQA5ADAAYwBmADQAZAA2AGQA" + + Decrypts the encrypted password which was stored in the TSV file. + #> + Param ( + [Parameter(Mandatory)] + [String]$EncryptedPassword, + + [Parameter(Mandatory)] + [String]$EncryptionKey + ) + + $Sha256 = new-object System.Security.Cryptography.SHA256CryptoServiceProvider + $SecureStringKey = $Sha256.ComputeHash([System.Text.UnicodeEncoding]::Unicode.GetBytes($EncryptionKey)) + + [SecureString]$SecureStringPassword = ConvertTo-SecureString -String $EncryptedPassword -Key $SecureStringKey + Write-Output ([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($SecureStringPassword))) + } + + ## The System.Web is not a default assembly. This must be loaded in order to generate the random password + Add-Type -AssemblyName "System.Web" -ErrorAction Stop +} + +process { + foreach ($Computer in $Computername) { + try { + $Properties = @{ + ComputerName = $Computer + Username = '' + Password = '' + PasswordType = '' + Result = '' + EnabledAccount = '' + } + if (!(Test-Connection -ComputerName $Computer -Quiet -Count 1)) { + Write-Warning "Computer '$Computer' is not online" + $Properties.Result = 'Offline' + [pscustomobject]$Properties | Export-Csv -Path $PasswordFilePath -Delimiter "`t" -Append -NoTypeInformation + } else { + $LocalUsers = Get-WmiObject -ComputerName $Computer -Class Win32_UserAccount -Filter "LocalAccount=true" + Write-Verbose "Found $($LocalUsers.Count) local users on $Computer" + foreach ($LocalUser in $LocalUsers) { + Write-Verbose "--Checking username $($LocalUser.Name) for administrator account" + $oUser = [ADSI]"WinNT://$Computer/$($LocalUser.Name), user" + $Sid = $oUser.objectSid.ToString().Replace(' ', '') + if ($Sid.StartsWith('1500000521') -and $Sid.EndsWith('4100')) { + Write-Verbose "--Username $($LocalUser.Name)|SID '$Sid' is the local administrator account" + $LocalAdministrator = $LocalUser + break + } + } + + $Properties.UserName = $LocalAdministrator.Name + Write-Verbose "Creating random password for $($LocalAdministrator.Name)" + $Password = Create-RandomPassword -PasswordLength $PasswordLength + if ($EncryptionKey) { + $Properties.PasswordType = 'Encrypted' + $Properties.Password = (Set-Encryption $Password $EncryptionKey) + } else { + $Properties.Password = $Password + $Properties.PasswordType = 'Unencrypted' + } + + $oUser.psbase.Invoke("SetPassword", $Password) + $Properties.Result = 'Success' + + + Write-Verbose "Checking to ensure local administrator '$($LocalAdministrator.Name)' is enabled" + if ($LocalAdministrator.Disabled) { + Write-Verbose "Local administrator '$($LocalAdministrator.Name)' is disabled. Enabling..." + $Properties.EnabledAccount = 'True' + $LocalAdministrator.Disabled = $false + $LocalAdministrator.Put() | Out-Null + } else { + $Properties.EnabledAccount = 'False' + Write-Verbose "Local administrator '$($LocalAdministrator.Name)' is already enabled." + } + + [pscustomobject]$Properties | Export-Csv -Path $PasswordFilePath -Delimiter "`t" -Append -NoTypeInformation + } + } catch { + Write-Error $_.Exception.Message + } + } +} \ No newline at end of file diff --git a/Set-ServiceAccount.ps1 b/Set-ServiceAccount.ps1 new file mode 100644 index 0000000..6b29216 --- /dev/null +++ b/Set-ServiceAccount.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS + Changes the account a service runs under +.NOTES + Created on: 11/28/2014 + Created by: Adam Bertram + Filename: Set-ServiceAccount.ps1 +.EXAMPLE + PS> .\Set-ServiceAccount.ps1 -ServiceName 'snmp' -Computername 'COMPUTER1','COMPUTER2' -Username someuser -Password password12 + + This example changes the account the service snmp runs under to someuser on the computers COMPUTER1 and COMPUTER2 +.PARAMETER ServiceName + One or more service names +.PARAMETER Computername + One or more remote computer names. This script defaults to the local computer. +.PARAMETER Username + The username to change on the service +.PARAMETER Password + The password of the username +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)] + [string[]]$ServiceName, + [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] + [ValidateScript({Test-Connection -ComputerName $_ -Quiet -Count 1 })] + [string[]]$Computername = 'localhost', + [Parameter(Mandatory)] + [string]$Username, + [Parameter(Mandatory)] + [string]$Password +) + +process { + foreach ($Computer in $Computername) { + foreach ($Service in $ServiceName) { + try { + Write-Verbose -Message "Changing service '$Service' on the computer '$Computer'" + $s = Get-WmiObject -ComputerName $Computer -Class Win32_Service -Filter "Name = '$Service'" + if (!$s) { + throw "The service '$Service' does not exist" + } + $s.Change($null, $null, $null, $null, $null, $null, $Username, $Password) | Out-Null + $s | Restart-Service -Force + } catch { + Write-Error -Message "Error: Computer: $Computer - Service: $Service - Error: $($_.Exception.Message)" + } + } + } +} \ No newline at end of file diff --git a/SetPrinterComments.ps1 b/SetPrinterComments.ps1 new file mode 100644 index 0000000..ad0a83d --- /dev/null +++ b/SetPrinterComments.ps1 @@ -0,0 +1,15 @@ +## Based off of a CSV, sets printer comments for a set of printers + +$Csv = Import-Csv "C:\Dropbox\Powershell\scripts\PrinterModelAndIPs.csv" +$server_printers = Get-WMIObject -Class "Win32_Printer" -NameSpace "root\cimv2" -computername ctxps +foreach ($row in $Csv) { + $model = $row.Model; + $ip = $row.IP; + $printer = $server_printers | ? {$_.comment -eq $ip } + if ($printer) { + #$printer | foreach {"Name: $($_.Name) | $($_.PortName) | $ip | $model"} + $printer | foreach { $_.Comment = "$ip $model"; $_.Put() | Out-Null} + + + } +} \ No newline at end of file diff --git a/Start-CMApplicationDeploymentTest.ps1 b/Start-CMApplicationDeploymentTest.ps1 new file mode 100644 index 0000000..50bb1c0 --- /dev/null +++ b/Start-CMApplicationDeploymentTest.ps1 @@ -0,0 +1,61 @@ +<# +.SYNOPSIS + +.NOTES + Created on: 8/22/2014 + Created by: Adam Bertram + Filename: + Credits: + Requirements: + Todos: +.EXAMPLE + +.EXAMPLE + +.PARAMETER PARAM1 + +.PARAMETER PARAM2 + +#> +[CmdletBinding(DefaultParameterSetName = 'name')] +[OutputType('System.Management.Automation.PSCustomObject')] +param ( + [Parameter(ParameterSetName = 'name', + Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [ValidateSet("Tom","Dick","Jane")] + [ValidateRange(21,65)] + [ValidateScript({Test-Path $_ -PathType 'Container'})] + [ValidateNotNullOrEmpty()] + [ValidateCount(1,5)] + [ValidateLength(1,10)] + [ValidatePattern()] + [string]$Computername = 'DEFAULTVALUE' +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + try { + + } catch { + Write-Error $_.Exception.Message + } +} + +process { + try { + + } catch { + Write-Error $_.Exception.Message + } +} + +end { + try { + + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Start-PostConfigmgrBackupTasks.ps1 b/Start-PostConfigmgrBackupTasks.ps1 new file mode 100644 index 0000000..e419300 --- /dev/null +++ b/Start-PostConfigmgrBackupTasks.ps1 @@ -0,0 +1,270 @@ +#Requires -Version 3 + +<# +.SYNOPSIS + This script checks a ConfigMgr site backup to ensure success and performs various post-backup functions that + back up other critical data that the built-in ConfigMgr site backup task does not. +.DESCRIPTION + This script checks any previous backup attempt ran within the last hour of this script running for a + a successful run if backup check is selected. It assumes you also have SSRS installed on the site DB server + and backs up both SSRS databases, exports out the SSRS encryption keys, backs up the export file and the + entire ReportingServices folder on the server. Once SSRS has been backed up, it then also copies the entire + SCCMContentLib folder, client install folder and the afterbackup.bat file to the destination backup folder path. + Once complete, it then attaches the log file it creates as part of the process and emails it out to the defined recipient. + + The script intends on creating 7 days worth of backups in a central location labeled Monday through Friday + and places a copy of all backed up components in each day's folder. +.NOTES + Created on: 6/13/2014 + Created by: Adam Bertram + Filename: Start-PostConfigMgrBackupSteps.ps1 + Credits: http://bit.ly/1i24NgC + Requirements: ConfigMgr, Reporting Point installed on the site DB server + Todos: Use the Sync framework to only copy deltas (http://bit.ly/1nh3FmP) + Backup custom updates added via SCUP + Verify copies were actually successful + Retrieve more params automatically +.EXAMPLE + .\Start-PostConfigMgrBackupSteps.ps1 + This example uses all default parameters for the script which will be the most likely way it is executed. +.PARAMETER SiteCode + The ConfigMgr site code that the site server is a part of +.PARAMETER SiteDbServer + The Configmgr site server that has the Reporting Services Point and the Site database server role installed. +.PARAMETER DestDbBackupFolderPath + The UNC root folder path where the days' backup folders will be copied to +.PARAMETER SrcReportingServicesFolderPath + The UNC folder path where you have installed reporting services to on the server. +.PARAMETER ReportingServicesDbBackupSqlFilePath + The UNC file path to the SQL file that is dynamically created (if not exists) that the script passes to the + sqlcmd utility to kick off a backup of the SSRS databases. This does not have to exist. It is recommended + to allow the script to create this. +.PARAMETER ReportingServicesEncKeyPassword + The password that's set on the exported SSRS keys +.PARAMETER SrcContentLibraryFolderPath + The folder path to ConfigMgr's content library on the site server. This folder is called SCCMContentLib. The + default path probably does not need to be changed. +.PARAMETER SrcClientInstallerFolderPath + The folder path where the ConfigMgr client install is located on the site server. This is backed up if you + have any hotfixes being installed with your clients and may be located in here. +.PARAMETER SrcAfterBackupFilePath + The file path where the afterbackup.bat file is located. You should not have to change this from the default. +.PARAMETER LogFilesFolderPath + The folder path where the script will create a log file for each day it runs. +.PARAMETER CheckBackup + Use this switch to first check to ensure a recent backup was successful. This parameter is recommended + when running inside the afterbackup.bat file. +#> +[CmdletBinding()] +param ( + [string]$SiteCode = 'UHP', + [ValidateScript({Test-Connection $_ -Quiet -Count 1})] + [string]$SiteDbServer = 'CONFIGMANAGER', + [ValidateScript({ Test-Path $_ -PathType 'Container' })] + [string]$DestDbBackupFolderPath = '\\sanstoragea\lt_archive\30_Days\ConfigMgr', + [ValidateScript({ Test-Path $_ -PathType 'Container' })] + [string]$SrcReportingServicesFolderPath = "\\$SiteDbServer\f$\Sql2012Instance\MSRS11.MSSQLSERVER\Reporting Services", + [string]$ReportingServicesDbBackupSqlFilePath = "\\$SiteDbServer\c$\ReportingServicesDbBackup.sql", + [string]$ReportingServicesEncKeyPassword = 'my_password', + [ValidateScript({ Test-Path $_ -PathType 'Container' })] + [string]$SrcContentLibraryFolderPath = "\\$SiteDbServer\f$\SCCMContentLib", + [ValidateScript({ Test-Path $_ -PathType 'Container' })] + [string]$SrcClientInstallerFolderPath = "\\$SiteDbServer\c$\Program Files\Microsoft Configuration Manager\Client", + [ValidateScript({ Test-Path $_ -PathType 'Leaf' })] + [string]$SrcAfterBackupFilePath = "\\$SiteDbServer\c$\Program Files\Microsoft Configuration Manager\inboxes\smsbkup.box\afterbackup.bat", + [string]$LogFilesFolderPath = "$DestDbBackupFolderPath\Logs", + [switch]$CheckBackup +) + +begin { + Set-StrictMode -Version Latest + try { + ## This function builds a SQL file called $ReportingServicesDbBackupSqlFile that backs up both + ## reporting services databases to a subfolder called ReportsBackup under today's + ## destination backup folder + function New-ReportingServicesBackupSqlFile($TodayDbDestFolderPath) { + Add-Content -Value "declare @path1 varchar(100); + declare @path2 varchar(100); + SET @path1 = '$TodayDbDestFolderPath\ReportsBackup\ReportServer.bak'; + SET @path2 = '$TodayDbDestFolderPath\ReportsBackup\ReportServerTempDB.bak'; + + USE ReportServer; + BACKUP DATABASE REPORTSERVER TO DISK = @path1; + BACKUP DATABASE REPORTSERVERTEMPDB TO DISK = @path2; + DBCC SHRINKFILE(ReportServer_log); + USE ReportServerTempDb; + DBCC SHRINKFILE(ReportServerTempDB_log);" -Path $ReportingServicesDbBackupSqlFilePath + } + + function Convert-ToLocalFilePath($UncFilePath) { + $Split = $UncFilePath.Split('\') + $FileDrive = $Split[3].TrimEnd('$') + $Filename = $Split[-1] + $FolderPath = $Split[4..($Split.Length - 2)] + if ($Split.count -eq 5) { + "$FileDrive`:\$Filename" + } else { + "$FileDrive`:\$FolderPath\$Filename" + } + } + + Function Get-LocalTime($UTCTime) { + $strCurrentTimeZone = (Get-WmiObject win32_timezone).StandardName + $TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById($strCurrentTimeZone) + $LocalTime = [System.TimeZoneInfo]::ConvertTimeFromUtc($UTCTime, $TZ) + $LocalTime + } + + if (!(Test-Path $LogFilesFolderPath)) { + New-Item -Path $LogFilesFolderPath -Type Directory | Out-Null + } + $script:MyDate = Get-Date -Format 'MM-dd-yyyy' + $script:LogFilePath = "$LogFilesFolderPath\$MyDate.log" + + ## Simple logging function to create a log file in $LogFilesFolderPath named today's + ## date then write a timestamp and the message on each line and outputs the log file + ## path it wrote to + function Write-Log($Message) { + $MyDateTime = Get-Date -Format 'MM-dd-yyyy H:mm:ss' + Add-Content -Path $script:LogFilePath -Value "$MyDateTime - $Message" + } + + $DefaultBackupFolderPath = "$DestDbBackupFolderPath\$SiteCode" + 'Backup' + if (!(Test-Path $DefaultBackupFolderPath)) { + throw "Default backup folder path $DefaultBackupFolderPath does not exist" + } + + if ($CheckBackup.IsPresent) { + ## Ensure the backup was successful before doing post-backup tasks + + ## $DefaultBackupFolderPath is the path where the builtin Site Backup SQL maintenance task places the + ## backup. Ensure it has today's write time before going further because if not then the backup + ## didn't run successfully + $BackupFolderLastWriteDate = (Get-ItemProperty $DefaultBackupFolderPath).Lastwritetime.Date + + $SuccessMessageId = 5035 + $OneHourAgo = (Get-Date).AddHours(-1) + Write-Log "One hour ago detected as $OneHourAgo" + + $WmiParams = @{ + 'ComputerName' = $SiteDbServer; + 'Namespace' = "root\sms\site_$SiteCode"; + 'Class' = 'SMS_StatusMessage'; + 'Filter' = "Component = 'SMS_SITE_BACKUP' AND MessageId = '$SuccessMessageId'" + } + $LastSuccessfulBackup = (Get-WmiObject @WmiParams | sort time -Descending | select -first 1 @{ n = 'DateTime'; e = { $_.ConvertToDateTime($_.Time) } }).DateTime + $LastSuccessfulBackup = Get-LocalTime $LastSuccessfulBackup + Write-Log "Last successful backup detected on $LastSuccessfulBackup" + $IsBackupSuccessful = $LastSuccessfulBackup -gt $OneHourAgo + + if (($BackupFolderLastWriteDate -ne (get-date).date) -or !$IsBackupSuccessful) { + throw 'The backup was not successful. Post-backup procedures not necessary' + } + } + + $CommonCopyFolderParams = @{ + 'Recurse' = $true; + 'Force' = $true; + } + + } catch { + Write-Log "ERROR: $($_.Exception.Message)" + exit (10) + } +} + +process { + try { + ## If today's folder exists in the root of the backup folder path + ## remove it else create a new one + $Today = (Get-Date).DayOfWeek + $TodayDbDestFolderPath = "$DestDbBackupFolderPath\$Today" + if ((Test-Path $TodayDbDestFolderPath -PathType 'Container')) { + Remove-Item $TodayDbDestFolderPath -Force -Recurse + Write-Log "Removed $TodayDbDestFolderPath..." + } + + ## Rename the default backup folder to today's day of the week + Rename-Item $DefaultBackupFolderPath $Today + ## Create the folder to put the reporting services database backups in + New-Item -Path "$TodayDbDestFolderPath\ReportsBackup" -ItemType Directory | Out-Null + + ## if the SQL file that gets invoked to backup the SSRS databases isn't in the root of + ## C on the site server, build it. The root of C isn't necessary. It just needs to be + ## somewhere on the local server + if (Test-Path $ReportingServicesDbBackupSqlFilePath) { + Remove-Item $ReportingServicesDbBackupSqlFilePath -Force + } + New-ReportingServicesBackupSqlFile $TodayDbDestFolderPath + Write-Log "Created new SQL file in $TodayDbDestFolderPath..." + + ## Convert the UNC path specified for the SQL file into a local path to feed to + ## sqlcmd on the site server which backs up the SSRS databases. Confirm success + ## afterwards. + Write-Log "Backing up SSRS Databases..." + $LocalPath = Convert-ToLocalFilePath $ReportingServicesDbBackupSqlFilePath + $result = Invoke-Command -ComputerName $SiteDbServer -ScriptBlock { sqlcmd -i $using:LocalPath } + if ($result[-1] -match 'DBCC execution completed') { + Write-Log 'Successfully backed up SSRS databases' + } else { + Write-Log 'WARNING: Failed to backup SSRS databases' + } + + ## Export the SSRS Encryption keys to a local file via remoting on the site server and + ## copy that file to the backup location + Write-Log "Exporting SSRS encryption keys..." + $ExportFilePath = "\\$SiteDbServer\c$\rsdbkey.snk" + $LocalPath = Convert-ToLocalFilePath $ExportFilePath + $result = Invoke-Command -ComputerName $SiteDbServer -ScriptBlock { echo y | rskeymgmt -e -f $using:LocalPath -p $using:ReportingServicesEncKeyPassword } + if ($result[-1] -ne 'The command completed successfully') { + Write-Log 'WARNING: SSRS keys were not exported!' + } else { + Copy-Item $ExportFilePath $TodayDbDestFolderPath -Force + Write-Log 'Successfully exported and backed up encryption keys.' + } + + ## Backup the Reporting Services SSRS folder + Write-Log "Backing up $SrcReportingServicesFolderPath..." + Copy-Item @CommonCopyFolderParams -Path $SrcReportingServicesFolderPath -Destination "$TodayDbDestFolderPath\ReportsBackup" + Write-Log "Successfully backed up the $SrcReportingServicesFolderPath folder.." + + ## Backup the SCCMContentLib folder + Write-Log "Backing up $SrcContentLibraryFolderPath..." + Copy-Item @CommonCopyFolderParams -Path $SrcContentLibraryFolderPath -Destination $TodayDbDestFolderPath + Write-Log "Successfully backed up the $SrcContentLibraryFolderPath folder.." + + ## Backup the client install folder from the site server to the backup folder + Write-Log "Backing up $SrcClientInstallerFolderPath..." + Copy-Item @CommonCopyFolderParams -Path $SrcClientInstallerFolderPath -Destination $TodayDbDestFolderPath + Write-Log "Successfully backed up the $SrcClientInstallerFolderPath folder.." + + ##TODO: Backup any SCUP updates + ## On the computer that runs Updates Publisher, browse the Updates Publisher 2011 database file (Scupdb.sdf) + ## in %USERPROFILE%\AppData\Local\Microsoft\System Center Updates Publisher 2011\5.00.1727.0000\. There is + ## a different database file for each user that runs Updates Publisher 2011. Copy the database file to your + ## backup destination. For example, if your backup destination is E:\ConfigMgr_Backup, you could copy the + ## Updates Publisher 2011 database file to E:\ConfigMgr_Backup\SCUP2011. + + ## Backup the afterbackup.bat file that kicks off this script + Write-Log "Backing up $SrcAfterBackupFilePath.." + Copy-Item @CommonCopyFolderParams -Path $SrcAfterBackupFilePath -Destination $TodayDbDestFolderPath + Write-Log "Successfully backed up the $SrcAfterBackupFilePath file..." + + } catch { + Write-Log "ERROR: $($_.Exception.Message)" + } +} + +end { + Write-Log 'Emailing results of backup...' + ## Email me the results of the backup and post-backup tasks + $Params = @{ + 'From' = 'UNH ConfigMgr Backup '; + 'To' = 'Adam Bertram '; + 'Subject' = 'UNH ConfigMgr Backup'; + 'Attachment' = $script:LogFilePath; + 'SmtpServer' = 'smtp.uhhg.org' + } + + Send-MailMessage @Params -Body 'ConfigMgr Backup Email' +} \ No newline at end of file diff --git a/Sync-CmToWsusSoftwareUpdates.ps1 b/Sync-CmToWsusSoftwareUpdates.ps1 new file mode 100644 index 0000000..7ffba1f --- /dev/null +++ b/Sync-CmToWsusSoftwareUpdates.ps1 @@ -0,0 +1,235 @@ +<# +.SYNOPSIS + This script retrieves all Configuration Manager updates that are deployed. It will then check + a WSUS server's updates and approve any WSUS updates that are in Configuration Manager. Optionally, + it can also decline any WSUS updates that are not in Configuration Manager as well as force a manual + MS Update --> WSUS sync if it finds any updates in Configuration Manager that are not in WSUS. +.NOTES + Created on: 1/29/15 + Created by: Adam Bertram + Filename: Sync-CmToWsusSoftwareUpdates.ps1 + Credits: http://blogs.technet.com/b/deploymentguys/archive/2009/10/22/approving-windows-updates-in-an-mdt-2010-standalone-environment-from-a-configmgr-software-update-point.aspx + http://configmgrblog.com/2013/10/26/migrate-approved-software-updates-wsus-configmgr-2012/ + http://myitforum.com/cs2/blogs/maikkoster/archive/2013/08/29/approving-updates-from-configmgr-scup-to-wsus.aspx + Requirements: The WSUS console must be installed - http://www.microsoft.com/en-us/download/details.aspx?id=5216 + Suggestions: WSUS: No updates are manually approved on the WSUS server + WSUS: Update source set to Microsoft Update + WSUS: Synchronization schedule is set to automatic + WSUS: Automatic Approvals are disabled + This script is meant to be ran as a scheduled task and as a right-click tool in ConfigMgr. +.EXAMPLE + PS> .\Sync-CmToWsusSoftwareUpdates.ps1 + + This example finds all deployed CM updates and attempts to match these updates against WSUS. If a match is found, it will + approve the WSUS update. +.EXAMPLE + PS> .\Sync-CmToWsusSoftwareUpdates.ps1 -DeclineAllNonMatches + + This example finds all deployed CM updates and attempts to match these updates against WSUS. If a match is found, it will + approve the WSUS update. If a WSUS update is not in CM, it will decline it. +.PARAMETER DeclineAllNonMatches + By default, the script will only approve matches in WSUS. If this switch parameter is used, it will also decline all + WSUS updates that do not have a match in CM. +.PARAMETER SyncWsus + By default, the script will report on any CM updates that it could not find a match to in WSUS. If this switch paramter is used + it will force a manual sync on the WSUS server from MS update in an effort to get any remaining updates onto the WSUS server. +.PARAMETER LogFilePath + The file path where you'd like to report the script's activity +.PARAMETER CmSiteServer + The name of the site server to query for updates. +.PARAMETER CmSiteCode + The 3-letter ConfigMgr site code +.PARAMETER WsusServer + The name of the WSUS server +.PARAMETER WsusServerPort + This is the port number that the WSUS server is listening on HTTP. It's typically 80 or 8530. +#> +[CmdletBinding()] +param ( + [switch]$DeclineAllNonMatches, + [switch]$SyncWsus, + [string]$LogFilePath = "$PsScriptRoot\SCCM-WSUSUpdateSync.log", + [ValidateScript({ Test-Connection -ComputerName $_ -Quiet -Count 1})] + [string]$CmSiteServer = '', + [string]$CmSiteCode = '', + [ValidateScript({ Test-Connection -ComputerName $_ -Quiet -Count 1 })] + [string]$WsusServer = '', + [string]$WsusServerPort = '8530' +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + + ## These functions would be nice to have but I could not immediately figure out how to find + ## the approved products and classifications in CM to sync with WSUS + <#function Sync-ClassificationSyncOptions { + Invoke-Command -ComputerName $WsusServer -ScriptBlock { Set-WsusClassification } + } + + function Sync-ProductSyncOptions { + Invoke-Command -ComputerName $WsusServer -ScriptBlock { Set-WsusProduct } + }#> + + function Write-Log { + <# + .SYNOPSIS + This function creates or appends a line to a log file + + .DESCRIPTION + This function writes a log line to a log file + .PARAMETER Message + The message parameter is the log message you'd like to record to the log file + .PARAMETER LogLevel + The logging level is the severity rating for the message you're recording. + You have 3 severity levels available; 1, 2 and 3 from informational messages + for FYI to critical messages. This defaults to 1. + + .EXAMPLE + PS C:\> Write-Log -Message 'Value1' -LogLevel 'Value2' + + This example shows how to call the Write-Log function with named parameters. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Message, + [ValidateSet(1, 2, 3)] + [int]$LogLevel = 1 + ) + + try { + [pscustomobject]@{ + 'Time' = Get-Date + 'Message' = $Message + 'ScriptLineNumber' = "$($MyInvocation.ScriptName | Split-Path -Leaf):$($MyInvocation.ScriptLineNumber)" + 'Severity' = $LogLevel + } | Export-Csv -Path $LogFilePath -Append -NoTypeInformation + } catch { + Write-Error $_.Exception.Message + $false + } + } + + function Get-MyWsusUpdate { + $Wsus.GetUpdates() + } + + function Get-AllComputerTargetGroup { + $Groups = $Wsus.GetComputerTargetGroups() + $Groups | where { $_.Name -eq 'All Computers' } + } + + function Approve-MyWsusUpdate ([Microsoft.UpdateServices.Internal.BaseApi.Update]$Update) { + $AllComputerTg = Get-AllComputerTargetGroup + $Update.Approve([Microsoft.UpdateServices.Administration.UpdateApprovalAction]::Install,$AllComputerTg) | Out-Null + } + + function Decline-MyWsusUpdate ([Microsoft.UpdateServices.Internal.BaseApi.Update]$Update) { + $Update.Decline() + } + + function Sync-WsusServer { + $Subscription = $Wsus.GetSubscription() + $Subscription.StartSynchronization() + } + + try { + Write-Log 'Loading the WSUS type and creating the WSUS server object...' + ## Load the type in order to query updates from the WSUS server. You must have the + ## WSUS administration console installed to get these assemblies + [void][reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") + $script:Wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($WsusServer, $false, $WsusServerPort) + } catch { + Write-Log "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3' + } + +} +process { + try { + Write-Log 'Finding all deployed CM updates...' + $DeployedCmUpdates = Get-CimInstance -ComputerName $CmSiteServer -Namespace "root\sms\site_$CmSiteCode" -Class SMS_SoftwareUpdate | where { $_.IsDeployed -and !$_.IsSuperseded -and !$_.IsExpired } + if (!$DeployedCmUpdates) { + throw 'Error retrieving CM updates' + } else { + Write-Log "Found $($DeployedCmUpdates.Count) deployed updates" + } + Write-Log "Finding all WSUS updates on the $WsusServer WSUS server..." + $WsusUpdates = Get-MyWsusUpdate + if (!$WsusUpdates) { + throw 'Error retrieving WSUS updates' + } + Write-Log "Found $($WsusUpdates.Count) applicable updates on the WSUS server" + Write-Log 'Beginning matching process...' + $MatchesMade = 0 + $NoMatchMade = 0 + $ApprovedMatches = 0 + $AlreadyApprovedMatches = 0 + $DeclinedWsusUpdates = 0 + foreach ($WsusUpdate in $WsusUpdates) { + try { + #Write-Log "Checking WSUS update $($WsusUpdate.Title) for a match..." + if ($DeployedCmUpdates.LocalizedDisplayname -contains $WsusUpdate.Title) { + #Write-Log "Found matching WSUS update '$($WsusUpdate.Title)'" + $MatchesMade++ + if (!$WsusUpdate.IsApproved) { + #Write-Log "Update is not approved. Checking for license agreement" + $ApprovedMatches++ + if ($WsusUpdate.HasLicenseAgreement) { + #Write-Log "Update has a license agreement. Accepting..." + $WsusUpdate.AcceptLicenseAgreement() + } else { + #Write-Log 'Update does not have a license agreement' + } + #Write-Log "Approving WSUS update..." + Approve-MyWsusUpdate -Update $WsusUpdate + $ApprovedMatches++ + } else { + #Write-Log "WSUS update is already approved." + $AlreadyApprovedMatches++ + } + } else { + #Write-Log 'No match found' + $NoMatchMade++ + if ($DeclineAllNonMatches.IsPresent) { + if ($WsusUpdate.IsDeclined) { + #Write-Log 'The WSUS update is already declined. No need to decline.' + } else { + #Write-Log 'Declining WSUS update...' + $DeclinedWsusUpdates++ + Decline-MyWsusUpdate -Update $WsusUpdate + } + } + } + } catch { + Write-Log "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3' + } + } + Write-Log 'Finding all CM updates that are not in WSUS...' + $CmUpdatesNotInWsus = $DeployedCmUpdates | where { $WsusUpdates.Title -notcontains $_.LocalizedDisplayName } + if (!$CmUpdatesNotInWsus) { + Write-Log 'No CM updates found with no match in WSUS' + } else { + foreach ($CmUpdate in $CmUpdatesNotInWsus) { + Write-Log "CM update '$($CmUpdate.LocalizedDisplayName)' not in WSUS" + } + if ($SyncWsus.IsPresent) { + ## Force a manual sync with Microsoft in an attempt to download the updates we're missing. We're + ## not tracking the sync but hopefully by the next CM --> WSUS sync, these updates will be there. + Write-Log 'Forcing a WSUS sync...' + Sync-WsusServer + } + } + Write-Log "---------------------------------------------" + Write-Log "WSUS Updates in CM: $MatchesMade" + Write-Log "WSUS Updates in CM Declined: $DeclinedWsusUpdates" + Write-Log "WSUS Updates in CM Already Approved: $AlreadyApprovedMatches" + Write-Log "WSUS Updates in CM Approved: $ApprovedMatches" + Write-Log "WSUS Updates not in CM: $NoMatchMade" + Write-Log "CM Updates not in WSUS: $($CmUpdatesNotInWsus.Count)" + Write-Log "---------------------------------------------" + Write-Log 'CM --> WSUS synchronization complete' + } catch { + Write-Log "Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" -LogLevel '3' + } +} \ No newline at end of file diff --git a/Sync-CsvToSql.ps1 b/Sync-CsvToSql.ps1 new file mode 100644 index 0000000..fb735b2 --- /dev/null +++ b/Sync-CsvToSql.ps1 @@ -0,0 +1,126 @@ +#Requires -Module SQLPSX + +<# +.SYNOPSIS + This queries compares the contents of a CSV file against a SQL database table + and either reports or syncs the different field values to the SQL table. + + If -replace is not used, it will output only the rows in the SQL database that are + different + + 2/16/15 CURRENT LIMITATION: This will only sync int, smallint, varchar and char fields. +.NOTES + Created on: 2/16/15 + Created by: Adam Bertram + Filename: Sync-CsvToSql.ps1 +.EXAMPLE + +.EXAMPLE +.PARAMETER Replace + Use this switch parameter to move from simply reporting on changes to making changes +.PARAMETER CsvFilePath + The path to the CSV file that will be read and compared against the SQL table +.PARAMETER ServerInstance + The server name and the instance name (if more than one installed) of the SQL server +.PARAMETER Database + The name of the database that contains the table you'll be syncing +.PARAMETER Schema + The database schema if other than dbo. +.PARAMETER Table + The name of the SQL table you'll be syncing the CSV's contents to +#> +[CmdletBinding()] +param ( + [switch]$Replace, + [Parameter(Mandatory)] + [ValidateScript({ Test-Path $_ -PathType Leaf })] + [string]$CsvFilePath, + [Parameter(Mandatory)] + [string]$ServerInstance, + [Parameter(Mandatory)] + [string]$Database, + [string]$Schema = 'dbo', + [Parameter(Mandatory)] + [string]$Table +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + try { + ## Ensure the columns in the CSV file and the database are equal + $CsvRows = Import-Csv -Path $CsvFilePath + $SqlTable = Get-SqlTable -Database (Get-SqlDatabase -sqlserver $Server -dbname $Database) -Name $Table -Schema $Schema + if (Compare-Object -DifferenceObject ($CsvRows[0].Psobject.Properties.Name) -ReferenceObject ($SqlTable.Columns.Name)) { + throw 'The field names in the CSV file and the SQL database are not equal' + } + + ## Load the entire table in memory for faster querying + $SqlRows = Get-SqlData -sqlserver $ServerInstance -dbname $Database -qry "SELECT * FROM $Table" + } catch { + Write-Error $_.Exception.Message + exit + } +} + +process { + try { + + ## Find the primary key to keep a 1:1 match + $PrimaryKey = ($SqlTable.Columns | Where-Object { $_.InPrimaryKey }).Name + ## I must separate the int and char columns out because if a string is detected in the CSV and that value + ## is attempted to be updated in the table in a non-char field, it will fail. This allows me + ## to treat the value as a string with quotes or not. + $SqlIntCols = $SqlTable.Columns | where { @('smallint', 'int') -contains $_.DataType.Name } + $SqlCharCols = $SqlTable.Columns | where { @('varchar', 'char') -contains $_.DataType.Name } + + foreach ($CsvRow in $CsvRows) { + try { + ## Find the match + $SqlRow = $SqlRows | Where-Object { $_.$PrimaryKey -eq $CsvRow.$PrimaryKey } + if ($SqlRow) { + #Write-Verbose "SQL row match found for row $($CsvRow.$PrimaryKey)" + #$FieldDiffs = [System.Collections.ArrayList]@() + $FieldDiffs = [ordered]@{ } + foreach ($CsvProp in ($CsvRow.PsObject.Properties | where { $_.Name -ne $PrimaryKey })) { + foreach ($SqlProp in ($SqlRow.PsObject.Properties | where { $_.Name -ne $PrimaryKey })) { + if (($CsvProp.Name -eq $SqlProp.Name) -and ($CsvProp.Value -ne $SqlProp.Value)) { + $FieldDiffs["$($CsvProp.Name) - FROM"] = $SqlProp.Value; + $FieldDiffs["$($CsvProp.Name) - TO"] = $CsvProp.Value + } + } + } + if (!($FieldDiffs.Keys | where { $_ })) { + Write-Verbose "All fields are equal for row $($SqlRow.$PrimaryKey)" + } else { + $FieldDiffs['PrimaryKeyValue'] = $SqlRow.$PrimaryKey + if (!$Replace.IsPresent) { + [pscustomobject]$FieldDiffs + } else { + # $UpdateString = 'UPDATE Directory SET ' + # foreach ($Diff in $FieldDiffs) { + # if ($Replace.IsPresent) { + # if ($SqlIntCols.Name -contains $Prop.Name) { + # $Items.Add("$($Prop.Name)=$($Prop.Value)") | Out-Null + # } elseif ($SqlCharCols.Name -contains $Prop.Name) { + # $Items.Add("$($Prop.Name)='$($Prop.Value)'") | Out-Null + # } + # $UpdateString += ($Items -join ',') + # $UpdateString += " WHERE $PrimaryKey = '$($CsvRow.$PrimaryKey)'" + # Set-SqlData @SqlParams -qry $UpdateString + # } else { + # $Fiel + # } + } + } + } else { + Write-Verbose "No SQL row match found for CSV row $($CsvRow.$PrimaryKey)" + } + } catch { + Write-Warning "Error Occurred: $($_.Exception.Message) in row $($CsvRow.$PrimaryKey)" + } + } + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Test-ClientDynamicDns.ps1 b/Test-ClientDynamicDns.ps1 new file mode 100644 index 0000000..4e40766 --- /dev/null +++ b/Test-ClientDynamicDns.ps1 @@ -0,0 +1,201 @@ +#Requires -Module DnsServer +#Requires -Version 3 + +<# +.SYNOPSIS + This script tests a particular computer to see if it's dynamic DNS functionality is working. +.DESCRIPTION + This script firsts check to ensure the any NIC on the specified computer has dynamic DNS enabled. If a NIC is found, it then proceeds + to find the client's primary DNS server. This will be the DNS server the script calls for when checking server-side to see if + it can update it's own record. It then finds the client's primary DNS suffix. This will be the zone the script checks on + the server. + + If the computer record's timestamp is still within the no-refresh period of the zone, the test cannot happen because the server + will disallow any updates to the timestamp. However, the script will still return true because the record is still considered "healthy". + + It then tries to issue an "ipconfig /registerdns" on the computer via remoting. If that fails, it then attempts to create a process + via WMI. It then waits a default of 10 seconds and checks the computer's DNS record at the server to see if the register DNS attempt + worked. If, after the retry interval time elapses, the record's timestamp still hasn't been updated, the script will return a failure. +.NOTES + Created on: 8/19/2014 + Created by: Adam Bertram + Filename: Test-ClientDynamicDns.ps1 +.EXAMPLE + PS> .\Test-ClientDynamicDns.ps1 -Computername COMPUTER1 -RetryInterval 20 + + In this example, the script will test the computername COMPUTER1 and will retry 20 times every second to see if the + client computer updated it's timestamp on it's DNS host record. +.EXAMPLE + PS> .\Test-ClientDynamicDns.ps1 -Computername COMPUTER1 + + In this example, the script will test the computername COMPUTER1 and will retry 10 times every second to see if the + client computer updated it's timestamp on it's DNS host record. +.PARAMETER Computername + The name of the computer you'd like to test dynamic DNS functionality on +.PARAMETER RetryInterval + The total time you'd like the script to check the computer's host DNS record for a current timestamp +#> +[CmdletBinding()] +[OutputType('System.Management.Automation.PSCustomObject')] +param ( + [Parameter(Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [ValidateScript({Test-Connection -ComputerName $_ -Quiet -Count 1})] + [string]$Computername, + [Parameter()] + [int]$RetryInterval = 10 +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + try { + ## http://www.leeholmes.com/blog/2009/11/20/testing-for-powershell-remoting-test-psremoting/ + function Test-PsRemoting { + param ( + [Parameter(Mandatory = $true)] + $computername + ) + + try { + Write-Verbose "Testing for enabled remoting" + $result = Invoke-Command -ComputerName $computername { 1 } + } catch { + return $false + } + + ## I’ve never seen this happen, but if you want to be + ## thorough…. + if ($result -ne 1) { + Write-Verbose "Remoting to $computerName returned an unexpected result." + return $false + } + $true + } + + function Get-ClientPrimaryDns ($NicIndex) { + Write-Verbose "Finding primary DNS server for client '$Computername'" + $Result = Get-WmiObject -ComputerName $Computername -Class win32_networkadapterconfiguration -Filter "IPenabled = $true AND Index = $NicIndex" + if ($Result) { + $PrimaryDnsServer = $Result.DNSServerSearchOrder[0] + Write-Verbose "Found computer '$Computername' primary DNS server as '$PrimaryDnsServer'" + $PrimaryDnsServer + } else { + $false + } + } + + function Get-ClientPrimaryDnsSuffix { + $Registry = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $Computername) + $RegistryKey = $Registry.OpenSubKey("SYSTEM\CurrentControlSet\Services\Tcpip\Parameters", $true) + $DnsSuffix = $RegistryKey.GetValue('NV Domain') + if ($DnsSuffix) { + Write-Verbose "Computer '$Computername' primary DNS suffix is '$DnsSuffix'" + $DnsSuffix + } else { + Write-Warning "Could not find primary DNS suffix on computer '$Computername'" + $false + } + } + + function Get-DynamicDnsEnabledNicIndex { + $EnabledIndex = Get-WmiObject -Class Win32_NetworkAdapterConfiguration -ComputerName $Computername -Filter { IPEnabled = 'True' } | where { $_.FullDNSRegistrationEnabled } + if (!$EnabledIndex) { + Write-Warning 'No NIC detected to have dynamic DNS enabled' + $false + } elseif ($EnabledIndex -is [array]) { + Write-Warning 'Multiple NICs detected having dynamic DNS enabled. This is not supported' + $false + } else { + Write-Verbose "Found NIC with index '$($EnabledIndex.Index)' as dynamic DNS enabled" + [int]$EnabledIndex.Index + } + } + + function Validate-IsInRefreshPeriod($Record) { + if ($Record.Timestamp.AddDays($ZoneAging.NoRefreshInterval.Days) -lt (Get-Date)) { + Write-Verbose 'The record is in the refresh period' + $true + } else { + Write-Verbose 'The record is not in the refresh period' + $false + } + } + + $ResultHash = @{ 'Computername' = $Computername; } + + $EnabledNicIndex = Get-DynamicDnsEnabledNicIndex + if ($EnabledNicIndex -isnot [int]) { + throw "Computer '$Computername' does not have dynamic DNS enabled on any interface or on more than 1 interface" + exit + } + + $PrimaryDnsServer = Get-ClientPrimaryDns $EnabledNicIndex + if (! $PrimaryDnsServer) { + throw "Could not find computer '$Computername' primary DNS server." + exit + } + $DnsZone = Get-ClientPrimaryDnsSuffix + if (! $DnsZone) { + throw "Could not find computer '$Computername' primary DNS suffix." + exit + } + $script:ZoneAging = Get-DnsServerZoneAging -Name $DnsZone -ComputerName $PrimaryDnsServer + + $Record = Get-DnsServerResourceRecord -ComputerName $PrimaryDnsServer -Name $Computername -RRType A -ZoneName $DnsZone -ea silentlycontinue + if ($Record -and !($Record.TimeStamp)) { + throw "The '$($Record.Hostname)' record is static and has no timestamp." + } elseif (!$Record) { + Write-Verbose "The '$Computername' record does not exist on the DNS server '$($PrimaryDnsServer)'." + } elseif (!(Validate-IsInRefreshPeriod $Record)) { + Write-Verbose "The '$($Record.Hostname)' record timestamp is still within the '$DnsZone' zone no-refresh period." + $ResultHash.Result = $true + [pscustomobject]$ResultHash + exit + } +} catch { + Write-Error $_.Exception.Message + break +} +} + +process { + try { + ## Need to round the time down to the nearest hour. This is because when the DNS record's timestamp is updated it will + ## always do this. + $NowRoundHourDown = ((Get-Date).Date).AddHours((Get-Date).Hour) + if (Test-PsRemoting $Computername) { + Write-Verbose "Remoting already enabled on $Computername" + Invoke-Command -ComputerName $Computername -ScriptBlock { ipconfig /registerdns } | Out-Null + } else { + Write-Warning "Remoting not enabled on $Computername. Will attempt to use WMI to create remote process" + if (([WMICLASS]"\\$Computername\Root\CIMV2:Win32_Process").create("ipconfig /registerdns").ReturnValue -ne 0) { + throw "Unable to successfully start remote process on '$Computername'" + } + } + Write-Verbose "Initiated DNS record registration on '$Computername'. Waiting for record to update on DNS server.." + ## Wait at least 5 seconds before even starting to give the record a chance to update. + Start-Sleep -Seconds 5 + for ($i = 0; $i -lt $RetryInterval; $i++) { + $Record = Get-DnsServerResourceRecord -ComputerName $PrimaryDnsServer -Name $Computername -RRType A -ZoneName $DnsZone -ea SilentlyContinue + if ($Record) { + $Timestamp = $Record.Timestamp + } + if ($Timestamp -eq $NowRoundHourDown) { + Write-Verbose "Host DNS record for '$Computername' matches current rounded time of $NowRoundHourDown" + $ResultHash.Result = $true + [pscustomobject]$ResultHash + exit + } else { + Write-Verbose "Host DNS record timestamp '$Timestamp' for '$Computername' does not match current rounded time of '$NowRoundHourDown'. Trying again..." + } + Start-Sleep -Seconds 1 + } + + $ResultHash.Result = $false + [pscustomobject]$ResultHash + } catch { + Write-Error $_.Exception.Message + } +} \ No newline at end of file diff --git a/Test-CmDeploymentClientStatus.ps1 b/Test-CmDeploymentClientStatus.ps1 new file mode 100644 index 0000000..8d33883 --- /dev/null +++ b/Test-CmDeploymentClientStatus.ps1 @@ -0,0 +1,149 @@ +#Requires -Module ConfigurationManager +#Requires -Version 3 + +<# +.SYNOPSIS + This script checks a SCCM deployment for the number of clients that have failed and if the deployment + has not started. If either is true then it will send a notification. +.NOTES + Created on: 8/22/2014 + Created by: Adam Bertram + Filename: Test-CmDeploymentClientStatus.ps1 +.EXAMPLE + PS> Test-CmDeploymentClientStatus -DeploymentID ABC0045G + + This example would get the percentage of failed client for deployment ID ABC0045G and if + the percentage was equal to or over the FailureThreshold it would send an email to + ToEmailAddress. +.PARAMETER DeploymentId + The SCCM deployment ID to check +.PARAMETER FailurePercentThreshold + The percentage of clients that must fail the deployment before an email is sent +.PARAMETER ToEmailAddress + The email address to send a notification email to (if necessary) +.PARAMETER FromEmailAddress + The email address to show as being sent from +.PARAMETER FromDisplayName + The name shown in most email clients as the email being sent from +.PARAMETER EmailSubject + The subject of the email +.PARAMETER EmailBody + If you'd like to include a snippet of text in the email body. Use this parameter. +.PARAMETER SmtpServer + The SMTP server to send the email through +.PARAMETER SiteServer + The name of the SCCM site server +.PARAMETER SiteCode + The SCCM site code + +#> +[CmdletBinding(DefaultParameterSetName = 'EmailNotification')] +param ( + [Parameter(Mandatory)] + [string]$DeploymentId, + [Parameter()] + [ValidateRange(1, 100)] + [int]$FailurePercentThreshold = 10, + [Parameter(ParameterSetName = 'EmailNotification')] + [string]$ToEmailAddress = 'replacethis@defaultemail.com', + [Parameter(ParameterSetName = 'EmailNotification')] + [string]$FromEmailAddress = 'Replace me', + [Parameter(ParameterSetName = 'EmailNotification')] + [string]$FromDisplayName = 'A Failed SCCM Deployment', + [Parameter(ParameterSetName = 'EmailNotification')] + [string]$EmailSubject = 'Failed SCCM Deployment', + [Parameter(ParameterSetName = 'EmailNotification')] + [string]$EmailBody, + [Parameter(ParameterSetName = 'EmailNotification')] + [string]$SmtpServer = 'replace.this.com', + [Parameter()] + [string]$SiteCode = 'UHP' +) + +begin { + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + Set-StrictMode -Version Latest + + function Send-FailNotification ($Method) { + switch ($Method) { + 'Email' { + $Params = @{ + 'From' = "$FromDisplayName <$FromEmailAddress>" + 'To' = $ToEmailAddress + 'Subject' = $EmailSubject + 'SmtpServer' = $SmtpServer + } + if (!$EmailBody) { + $Params.Body = "$($Summary.PercentageFailed) percent of clients failed deployment ID $DeploymentId`n`r$Summary" + } else { + $Params.Body = $EmailBody + } + Send-MailMessage @Params + } + default { + $false + } + } + } + + function Get-ClientPercentage ($ClientGroup) { + [math]::Round((($Deployment.$ClientGroup / $Deployment.NumberTargeted) * 100), 1) + } + + function Get-ClientStatusSummary { + $script:Deployment = Get-CMDeployment -DeploymentId $DeploymentId + if (!$Deployment) { + $false + } else { + $Object = @{ + 'NumberTargeted' = $Deployment.NumberTargeted + 'NumberUnknown' = $Deployment.NumberUnknown + 'PercentageFailed' = Get-ClientPercentage 'NumberErrors' + 'PercentageInProgress' = Get-ClientPercentage 'NumberInProgress' + 'PercentageOther' = Get-ClientPercentage 'NumberOther' + 'PercentageSuccess' = Get-ClientPercentage 'NumberSuccess' + 'PercentageUnknown' = Get-ClientPercentage 'NumberUnknown' + } + [pscustomobject]$Object + } + } + + try { + Write-Verbose 'Checking to see if the SCCM module is available...' + if (!(Test-Path "$(Split-Path $env:SMS_ADMIN_UI_PATH -Parent)\ConfigurationManager.psd1")) { + throw 'Configuration Manager module not found. Is the admin console intalled?' + } elseif (!(Get-Module 'ConfigurationManager')) { + Write-Verbose 'The SCCM module IS available.' + Import-Module "$(Split-Path $env:SMS_ADMIN_UI_PATH -Parent)\ConfigurationManager.psd1" + } + $Location = (Get-Location).Path + Set-Location "$($SiteCode):" + + } catch { + Write-Error $_.Exception.Message + } +} + +process { + try { + Write-Verbose "Getting a summary of all client activity for deployment ID $DeploymentId..." + $script:Summary = Get-ClientStatusSummary + if (!$Summary) { + throw "Could not find deployment ID '$DeploymentId'" + } elseif ($Summary.PercentageFailed -ge $FailurePercentThreshold) { + Write-Verbose "Failure threshold breached. There are $($Summary.PercentageFailed) percent of clients that failed this deployment" + Send-FailNotification -Method 'Email' + } elseif ($Summary.NumberTargeted -eq $Summary.NumberUnknown) { + Write-Verbose 'It appears the deployment has not started' + Send-FailNotification -Method 'Email' + } else { + Write-Verbose "Failure threshold not breached. There are only $($Summary.PercentageFailed) percent of clients that failed this deployment" + } +} catch { + Write-Error $_.Exception.Message +} +} + +end { + Set-Location $Location +} \ No newline at end of file diff --git a/Test-PotentialScomAgent.ps1 b/Test-PotentialScomAgent.ps1 new file mode 100644 index 0000000..8c42904 --- /dev/null +++ b/Test-PotentialScomAgent.ps1 @@ -0,0 +1,120 @@ +function Test-Port { + <# + .SYNOPSIS + This function tests for open TCP/UDP ports. + .DESCRIPTION + This function tests any TCP/UDP port to see if it's open or closed. + .NOTES + Known Issue: If this function is called within 10-20 consecutively on the same port + and computer, the UDP port check will output $false when it can be + $true. I haven't figured out why it does this. + .PARAMETER Computername + One or more remote, comma-separated computer names + .PARAMETER Port + One or more comma-separated port numbers you'd like to test. + .PARAMETER Protocol + The protocol (UDP or TCP) that you'll be testing + .PARAMETER TcpTimeout + The number of milliseconds that the function will wait until declaring + the TCP port closed. + .PARAMETER + The number of millieconds that the function will wait until declaring + the UDP port closed. + .EXAMPLE + PS> Test-Port -Computername 'LABDC','LABDC2' -Protocol TCP 80,443 + + This example tests the TCP network ports 80 and 443 on both the LABDC + and LABDC2 servers. + #> + [CmdletBinding(DefaultParameterSetName = 'TCP')] + [OutputType([System.Management.Automation.PSCustomObject])] + param ( + [Parameter(Mandatory)] + [string[]]$ComputerName, + [Parameter(Mandatory)] + [int[]]$Port, + [Parameter(Mandatory)] + [ValidateSet('TCP', 'UDP')] + [string]$Protocol, + [Parameter(ParameterSetName = 'TCP')] + [int]$TcpTimeout = 1000, + [Parameter(ParameterSetName = 'UDP')] + [int]$UdpTimeout = 1000 + ) + process { + foreach ($Computer in $ComputerName) { + foreach ($Portx in $Port) { + $Output = @{ 'Computername' = $Computer; 'Port' = $Portx; 'Protocol' = $Protocol; 'Result' = '' } + Write-Verbose "$($MyInvocation.MyCommand.Name) - Beginning port test on '$Computer' on port '$Protocol`:$Portx'" + if ($Protocol -eq 'TCP') { + $TcpClient = New-Object System.Net.Sockets.TcpClient + $Connect = $TcpClient.BeginConnect($Computer, $Portx, $null, $null) + $Wait = $Connect.AsyncWaitHandle.WaitOne($TcpTimeout, $false) + if (!$Wait) { + $TcpClient.Close() + Write-Verbose "$($MyInvocation.MyCommand.Name) - '$Computer' failed port test on port '$Protocol`:$Portx'" + $Output.Result = $false + } else { + $TcpClient.EndConnect($Connect) + $TcpClient.Close() + Write-Verbose "$($MyInvocation.MyCommand.Name) - '$Computer' passed port test on port '$Protocol`:$Portx'" + $Output.Result = $true + } + $TcpClient.Close() + $TcpClient.Dispose() + } elseif ($Protocol -eq 'UDP') { + $UdpClient = New-Object System.Net.Sockets.UdpClient + $UdpClient.Client.ReceiveTimeout = $UdpTimeout + $UdpClient.Connect($Computer, $Portx) + Write-Verbose "$($MyInvocation.MyCommand.Name) - Sending UDP message to computer '$Computer' on port '$Portx'" + $a = new-object system.text.asciiencoding + $byte = $a.GetBytes("$(Get-Date)") + [void]$UdpClient.Send($byte, $byte.length) + #IPEndPoint object will allow us to read datagrams sent from any source. + Write-Verbose "$($MyInvocation.MyCommand.Name) - Creating remote endpoint" + $remoteendpoint = New-Object system.net.ipendpoint([system.net.ipaddress]::Any, 0) + try { + #Blocks until a message returns on this socket from a remote host. + Write-Verbose "$($MyInvocation.MyCommand.Name) - Waiting for message return" + $receivebytes = $UdpClient.Receive([ref]$remoteendpoint) + [string]$returndata = $a.GetString($receivebytes) + If ($returndata) { + Write-Verbose "$($MyInvocation.MyCommand.Name) - '$Computer' passed port test on port '$Protocol`:$Portx'" + $Output.Result = $true + } + } catch { + Write-Verbose "$($MyInvocation.MyCommand.Name) - '$Computer' failed port test on port '$Protocol`:$Portx' with error '$($_.Exception.Message)'" + $Output.Result = $false + } + $UdpClient.Close() + $UdpClient.Dispose() + } + [pscustomobject]$Output + } + } + } +} + +$Computername = '' +$RequiredSerices = 'Netlogon', 'RemoteRegistry' +$RequiredPorts = @( + @{ 'Protocol' = 'TCP'; 'Port' = 5723 }, + @{ 'Protocol' = 'UDP'; 'Port' = 5723 }, + @{ 'Protocol' = 'TCP'; 'Port' = 135 }, + @{ 'Protocol' = 'UDP'; 'Port' = 135 }, + @{ 'Protocol' = 'TCP'; 'Port' = 137 }, + @{ 'Protocol' = 'UDP'; 'Port' = 137 }, + @{ 'Protocol' = 'TCP'; 'Port' = 139 }, + @{ 'Protocol' = 'UDP'; 'Port' = 139 }, + @{ 'Protocol' = 'TCP'; 'Port' = 445 } +) + +foreach ($Port in $RequiredPorts) { + Test-Port -ComputerName $Computername -Protocol $Port.Protocol -Port $Port.Port +} + +## DNS hostname works forward and back + +## TCP/UDP port 5723,135,137,139 is open and the remote administration (RPC) rule is added, TCP/445 is open + +## the SCOM action account is a local admin somehow \ No newline at end of file diff --git a/Test-ServerRolePortGroup.ps1 b/Test-ServerRolePortGroup.ps1 new file mode 100644 index 0000000..d4be7cb --- /dev/null +++ b/Test-ServerRolePortGroup.ps1 @@ -0,0 +1,167 @@ +<# +.SYNOPSIS + This function tests for open TCP/UDP ports by server role. +.DESCRIPTION + This function tests for all the approprite TCP/UDP ports by server role so you don't have + to memorize or look up all of the ports that need to be tested for every time + you want to verify remote connectivity on a specific server role. +.NOTES + Link port references: + http://technet.microsoft.com/en-us/library/dd772723(v=ws.10).aspx + http://en.wikipedia.org/wiki/Server_Message_Block + http://technet.microsoft.com/en-us/library/cc940063.aspx +.PARAMETER Computername + One or more remote, comma-separated computer names +.PARAMETER ServerRole + The services on the computer that you'd like to find open ports for. This can be + common services like WinRm, Smb, Dns, Active Directory and NetBIOS +.EXAMPLE + PS> Test-ServerRolePortGroup -Computername 'LABDC','LABDC2' -ServerRole NetBIOS,WinRm,Dns + + This example tests the network ports necessary for NetBIOS, WinRm and Dns + to operate on the servers LABDC and LABDC2. +#> + +[CmdletBinding()] +[OutputType([System.Management.Automation.PSCustomObject])] +param ( + [Parameter(Mandatory)] + [ValidateScript({ Test-Connection -ComputerName $_ -Count 1 -Quiet})] + [string[]]$Computername, + [Parameter(Mandatory)] + [ValidateSet('WinRm','Smb','Dns','ActiveDirectoryGeneral','ActiveDirectoryGlobalCatalog','NetBios')] + [string[]]$ServerRole +) +begin { + + function Test-Port { + <# + .SYNOPSIS + This function tests for open TCP/UDP ports. + .DESCRIPTION + This function tests any TCP/UDP port to see if it's open or closed. + .NOTES + Known Issue: If this function is called within 10-20 consecutively on the same port + and computer, the UDP port check will output $false when it can be + $true. I haven't figured out why it does this. + .PARAMETER Computername + One or more remote, comma-separated computer names + .PARAMETER Port + One or more comma-separated port numbers you'd like to test. + .PARAMETER Protocol + The protocol (UDP or TCP) that you'll be testing + .PARAMETER TcpTimeout + The number of milliseconds that the function will wait until declaring + the TCP port closed. + .PARAMETER + The number of millieconds that the function will wait until declaring + the UDP port closed. + .EXAMPLE + PS> Test-Port -Computername 'LABDC','LABDC2' -Protocol TCP 80,443 + + This example tests the TCP network ports 80 and 443 on both the LABDC + and LABDC2 servers. + #> + [CmdletBinding(DefaultParameterSetName = 'TCP')] + [OutputType([System.Management.Automation.PSCustomObject])] + param ( + [Parameter(Mandatory)] + [string[]]$ComputerName, + [Parameter(Mandatory)] + [int[]]$Port, + [Parameter(Mandatory)] + [ValidateSet('TCP', 'UDP')] + [string]$Protocol, + [Parameter(ParameterSetName = 'TCP')] + [int]$TcpTimeout = 1000, + [Parameter(ParameterSetName = 'UDP')] + [int]$UdpTimeout = 1000 + ) + process { + foreach ($Computer in $ComputerName) { + foreach ($Portx in $Port) { + $Output = @{ 'Computername' = $Computer; 'Port' = $Portx; 'Protocol' = $Protocol; 'Result' = '' } + Write-Verbose "$($MyInvocation.MyCommand.Name) - Beginning port test on '$Computer' on port '$Protocol`:$Portx'" + if ($Protocol -eq 'TCP') { + $TcpClient = New-Object System.Net.Sockets.TcpClient + $Connect = $TcpClient.BeginConnect($Computer, $Portx, $null, $null) + $Wait = $Connect.AsyncWaitHandle.WaitOne($TcpTimeout, $false) + if (!$Wait) { + $TcpClient.Close() + Write-Verbose "$($MyInvocation.MyCommand.Name) - '$Computer' failed port test on port '$Protocol`:$Portx'" + $Output.Result = $false + } else { + $TcpClient.EndConnect($Connect) + $TcpClient.Close() + Write-Verbose "$($MyInvocation.MyCommand.Name) - '$Computer' passed port test on port '$Protocol`:$Portx'" + $Output.Result = $true + } + $TcpClient.Close() + $TcpClient.Dispose() + } elseif ($Protocol -eq 'UDP') { + $UdpClient = New-Object System.Net.Sockets.UdpClient + $UdpClient.Client.ReceiveTimeout = $UdpTimeout + $UdpClient.Connect($Computer, $Portx) + Write-Verbose "$($MyInvocation.MyCommand.Name) - Sending UDP message to computer '$Computer' on port '$Portx'" + $a = new-object system.text.asciiencoding + $byte = $a.GetBytes("$(Get-Date)") + [void]$UdpClient.Send($byte, $byte.length) + #IPEndPoint object will allow us to read datagrams sent from any source. + Write-Verbose "$($MyInvocation.MyCommand.Name) - Creating remote endpoint" + $remoteendpoint = New-Object system.net.ipendpoint([system.net.ipaddress]::Any, 0) + try { + #Blocks until a message returns on this socket from a remote host. + Write-Verbose "$($MyInvocation.MyCommand.Name) - Waiting for message return" + $receivebytes = $UdpClient.Receive([ref]$remoteendpoint) + [string]$returndata = $a.GetString($receivebytes) + If ($returndata) { + Write-Verbose "$($MyInvocation.MyCommand.Name) - '$Computer' passed port test on port '$Protocol`:$Portx'" + $Output.Result = $true + } + } catch { + Write-Verbose "$($MyInvocation.MyCommand.Name) - '$Computer' failed port test on port '$Protocol`:$Portx' with error '$($_.Exception.Message)'" + $Output.Result = $false + } + $UdpClient.Close() + $UdpClient.Dispose() + } + [pscustomobject]$Output + } + } + } + } + + $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop + + $PortGroups = @{ + 'WinRm' = @{ 'TCP' = 5985} + 'Smb' = @{ 'TCP' = 445; 'UDP' = 445 } + 'Dns' = @{ 'TCP' = 53; 'UDP' = 53 } + 'ActiveDirectoryGeneral' = @{ 'TCP' = 25, 88, 389, 464, 636, 5722, 9389; 'UDP' = 88,123,389,464 } + 'ActiveDirectoryGlobalCatalog' = @{ 'TCP' = 3268, 3269 } + 'NetBios' = @{ 'TCP' = 135, 137, 138, 139; 'UDP' = 137,138,139 } + } +} +process { + foreach ($Computer in $Computername) { + Write-Verbose "Beginning port tests on computer '$Computer'" + try { + $TestPortGroups = $PortGroups.GetEnumerator() | where { $ServerRole -contains $_.Key } + Write-Verbose "Found '$($TestPortGroups.Count)' port group(s) to test" + foreach ($PortGroup in $TestPortGroups) { + $PortGroupName = $PortGroup.Key + $PortGroupValues = $PortGroup.Value + foreach ($Value in $PortGroupValues.GetEnumerator()) { + $Protocol = $Value.Key + $Ports = $Value.Value + $TestResult = Test-Port -ComputerName $Computer -Protocol $Protocol -Port $Ports + $TestResult | Add-Member -MemberType 'NoteProperty' -Name 'PortSet' -Value $PortGroupName + $TestResult + } + } + } catch { + Write-Verbose "$($MyInvocation.MyCommand.Name) - Computer: $Computer - Error: $($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" + $false + } + } +} \ No newline at end of file diff --git a/TimeframeInvestigator.ps1 b/TimeframeInvestigator.ps1 new file mode 100644 index 0000000..dc4060d --- /dev/null +++ b/TimeframeInvestigator.ps1 @@ -0,0 +1,142 @@ +#requires -version 3 + +<# +.SYNOPSIS + This script searches a Windows computer for all event log or text log entries + between a specified start and end time. +.DESCRIPTION + This script enumerates all event logs and all text logs on a local or remote + Windows computer. It looks for any entries between a specified start and end + time. It then copies this activity to an output location for further analysis. +.EXAMPLE + SCRIPTNAME -StartTimestamp '01-29-2014 13:25:00' -EndTimeStamp '01-29-2014 13:28:00' -ComputerName COMPUTERNAME -OutputDirectory $desktop\trouble_logs -SkipEventLog Security +.PARAMETER computername + The computer name to query. Just one. +.PARAMETER logname + The name of a file to write failed computer names to. Defaults to errors.txt. +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory)] + [datetime]$StartTimestamp, + [Parameter(Mandatory)] + [datetime]$EndTimestamp, + [Parameter(ValueFromPipeline, + ValueFromPipelineByPropertyName)] + [string]$ComputerName = 'localhost', + [Parameter()] + [string]$OutputDirectory = ".\$Computername", + [Parameter()] + [string]$LogAuditFilePath = "$OutputDirectory\LogActivity.csv", + [Parameter()] + [switch]$EventLogsOnly, + [Parameter()] + [switch]$LogFilesOnly, + [Parameter()] + [string[]]$ExcludeDirectory, + [Parameter()] + [string[]]$FileExtension = @('log', 'txt', 'wer') +) + +begin { + $LogsFolderPath = "$OutputDirectory\logs" + if (!(Test-Path $LogsFolderPath)) { + mkdir $LogsFolderPath | Out-Null + } + + function Add-ToLog($FilePath,$LineText,$LineNumber,$MatchType) { + $Audit = @{ + 'FilePath' = $FilePath; + 'LineText' = $LineText + 'LineNumber' = $LineNumber + 'MatchType' = $MatchType + } + [pscustomobject]$Audit | Export-Csv -Path $LogAuditFilePath -Append -NoTypeInformation + } +} + +process { + + if (!($LogFilesOnly.IsPresent)) { + $Logs = (Get-WinEvent -ListLog * -ComputerName $ComputerName | where { $_.RecordCount }).LogName + $FilterTable = @{ + 'StartTime' = $StartTimestamp + 'EndTime' = $EndTimestamp + 'LogName' = $Logs + } + + $Events = Get-WinEvent -ComputerName $ComputerName -FilterHashtable $FilterTable -ea 'SilentlyContinue' + Write-Verbose "Found $($Events.Count) total events" + + ## Convert the properties to something friendlier + $LogProps = @{ } + [System.Collections.ArrayList]$MyEvents = @() + foreach ($Event in $Events) { + $LogProps.Time = $Event.TimeCreated + $LogProps.Source = $Event.ProviderName + $LogProps.EventId = $Event.Id + $LogProps.Message = $Event.Message.Replace("`n", '|').Replace("`r", '|') + $LogProps.EventLog = $Event.LogName + $MyEvents.Add([pscustomobject]$LogProps) | Out-Null + } + $MyEvents | sort Time | Export-Csv -Path "$OutputDirectory\eventlogs.txt" -Append -NoTypeInformation + } + + if (!($EventLogsOnly.IsPresent)) { + ## Enumerate all shares + $Shares = Get-WmiObject -ComputerName $ComputerName -Class Win32_Share | where { $_.Path -match '^\w{1}:\\$' } + [System.Collections.ArrayList]$AccessibleShares = @() + foreach ($Share in $Shares) { + $Share = "\\$ComputerName\$($Share.Name)" + if (!(Test-Path $Share)) { + Write-Warning "Unable to access the '$Share' share on '$Computername'" + } else { + $AccessibleShares.Add($Share) | Out-Null + } + } + + $AllFilesQueryParams = @{ + Path = $AccessibleShares + Recurse = $true + Force = $true + ErrorAction = 'SilentlyContinue' + File = $true + } + if ($ExcludeDirectory) { + $AllFilesQueryParams.ExcludeDirectory = $ExcludeDirectory + } + ##TODO: Add capability to match on Jan,Feb,Mar,etc + $DateTimeRegex = "($($StartTimestamp.Month)[\\.\-/]?$($StartTimestamp.Day)[\\.\-/]?[\\.\-/]$($StartTimestamp.Year))|($($StartTimestamp.Year)[\\.\-/]?$($StartTimestamp.Month)[\\.\-/]?[\\.\-/]?$($StartTimestamp.Day))" + Get-ChildItem @AllFilesQueryParams | where { $_.Length -ne 0 } | foreach { + try { + Write-Verbose "Processing file '$($_.Name)'" + if (($_.LastWriteTime -ge $StartTimestamp) -and ($_.LastWriteTime -le $EndTimestamp)) { + Write-Verbose "Last write time within timeframe for file '$($_.Name)'" + Add-ToLog -FilePath $_.FullName -MatchType 'LastWriteTime' + } + if ($FileExtension -contains $_.Extension.Replace('.','') -and !((Get-Content $_.FullName -Encoding Byte -TotalCount 1024) -contains 0)) { + ## Check the contents of text file to references to dates in the timeframe + Write-Verbose "Checking log file '$($_.Name)' for date/time match in contents" + $LineMatches = Select-String -Path $_.FullName -Pattern $DateTimeRegex + if ($LineMatches) { + Write-Verbose "Date/time match found in file '$($_.FullName)'" + foreach ($Match in $LineMatches) { + Add-ToLog -FilePath $_.FullName -LineNumber $Match.LineNumber -LineText $Match.Line -MatchType 'Contents' + } + ## I must create the directory ahead of time if it doesn't exist because + ## Copy-Item doesn't have the ability to automatically create the directory + $Trim = $_.FullName.Replace("\\$Computername\", '') + $Destination = "$OutputDirectory\$Trim" + if (!(Test-Path $Destination)) { + ##TODO: Remove the error action when long path support is implemented + mkdir $Destination -ErrorAction SilentlyContinue | Out-Null + } + Copy-Item -Path $_.FullName -Destination $Destination -ErrorAction SilentlyContinue -Recurse + } + } + } catch { + Write-Warning $_.Exception.Message + } + } + } +}