Labels

Showing posts with label Scripting. Show all posts
Showing posts with label Scripting. Show all posts

Sunday, May 9, 2010

Keeping Citrix sessions alive with sendkeys

I use the following script as a way of keeping my Citrix session alive to thwart security guys and their over-aggressive timeouts.

Normally I don't like to use sendkeys, but this seemed like the perfect way to trick ica into thinking keys were being pressed...

PowerShell and VBScript versions below.



#
# Find the wfica.exe process and poke keystrokes at it, preventing a Citrix session from timing out
# Be careful what you have open and what keystrokes you poke

[System.Object[]]$processes = get-process | where {$_.ProcessName -eq "wfica32.exe"}
if ($processes.count -ge 1) { $process = $processes[0] }

if ($process -is [System.Diagnostics.Process])
{
  $processes | format-list -property *

  for (;;)
  {
    [void] [System.Reflection.Assembly]::LoadWithPartialName("'Microsoft.VisualBasic")

    [Microsoft.VisualBasic.Interaction]::AppActivate($process.Id)
    start-sleep -seconds 1

    [void] [System.Reflection.Assembly]::LoadWithPartialName("'System.Windows.Forms")
    [System.Windows.Forms.SendKeys]::SendWait("1")
    start-sleep -seconds 240
  }

}





Set objWshShell = CreateObject("Wscript.Shell")

strProcessName = "wfica32.exe"

intProcessID = 0
Call FindProcessID(strProcessName, intProcessID)

if (intProcessID > 0) Then
 wscript.echo "Found process ID: " & intProcessID

 Do 
  objWshShell.AppActivate(intProcessID)
  WScript.Sleep 1000
  objWshShell.SendKeys "1"
  WScript.Sleep 240000
 Loop

End If


Function FindProcessID(ByRef strProcessName, ByRef intProcessID)

 strQuery = "Select * from Win32_Process Where Name = '" & strProcessName & "'" ' WQL query string looking for the specified process

 Set objNameSpace = GetObject("winmgmts://./root/cimv2")    ' The cimv2 namespace of the local machine

 FindProcessID = vbFalse
 Set objProcessSet = objNameSpace.ExecQuery(strQuery)     ' Execute the query
 For Each objProcess in objProcessSet      ' For each process in the set, ordered by oldest to newest
  intProcessID = objProcess.ProcessID
  WScript.Echo "Found process, ID: " & intProcessID
  WScript.Echo objProcess.Name & ", " & intProcessID & ", " & objProcess.CreationDate & ", " & objProcess.CommandLine & ", " & objProcess.Priority & ", " & objProcess.WorkingSetSize & ", " & objProcess.PageFileUsage
  FindProcessID = vbTrue
 Next

 Set objNameSpace = Nothing : Set objProcessSet = Nothing : Set objProcess = Nothing
End Function


Wayne's World of IT (WWoIT), Copyright 2010 Wayne Martin. 


Read more!

Sunday, November 2, 2008

OpsMgr 2007 performance script - VMware datastores

This post provides a method of collecting VirtualCenter datastore information for ESX hosts in one or more clusters as performance data in Operations Manager 2007. You can then trend datastore usage for longer term analysis, and alert on the data for proactive problem management. This is my first attempt at using script to gather performance data in OpsMgr 2007, and my lack of OpsMgr knowledge combined with the limited documentation/examples made this difficult and possibly harder than it needs to be.

Background

In OpsMgr 2007, I thought it would be useful to report on LUN disk space on the ESX side of things in a similar fashion to agent-based Windows disk performance counters, and while this sounds quite easy, I couldn’t find a simple method because:

  1. ESX doesn’t seem to support an SNMP trap to report on VMFS volume capacity/free space. You could write a shell script and add a cron job on the Linux ESX service console, but this would be targeted at each ESX server, and the results would then be duplicated (and wouldn't work with ESX 3i)
  2. VirtualCenter doesn’t appear to support a custom alert based on storage usage, only storage transfer rates.
  3. The ESX servers are only in OpsMgr as network devices, not true agents, so we can’t use WBEM or any other method to query directly. If the upcoming OpsMgr 2007 cross platform extensions work on ESX, this may be easier, although as with option 1 this would still return duplicate datastore information for each ESX server in a cluster and wouldn't work with ESX 3i)

This left a few options to gather the data in OpsMgr:

  1. Query the VMFS storage through the VI snap-in and a PowerShell Script.
  2. Query the VC database directly for this information from a VBS or PowerShell script.
  3. Create a DTS job on the VC SQL server to extract the data to a suitable format.

Options two and three above would not be supported by VMware and may change as the database structure changes between VC versions.

Solution

Querying the VMFS storage from PowerShell and collect the information with VBScript, which includes:

  1. The PowerShell script using the VI snap-in to extract the datastore information
  2. The VBScript to gather the information as performance data in Operations Manager 2007
  3. A trigger to execute the powershell script and put the data in a file to be gathered.
  4. Rules in OpsMgr to gather the data using the type 'Script (Performance)'.

Steps taken:

  1. Install the VMware VI PowerShell snap-in to your Virtual Centre box (you may need to install PowerShell first). VMware-Vim4PS-1.0.0-113525.exe is the binary I used.
  2. Create an AD group and user for rights to VC. Add the user to the group.
  3. Set rights in VirtualCenter to allow the group read-only rights at the root. This should be more granular if possible.
  4. Add a recurring trigger on your VC box to run the PowerShell script to extract the information. Eg, this could be a scheduled task that runs a batch file or directly executes:
    1. powershell . ".\PerfGatherVMwareDSSpace.ps1"
  5. Create two rules in Operations Manager, Script (Performance) to gather the daily log file and store in the database as performance data. One rule is for the free space, another is for the free space as a percentage of the capacity. See the performance mapper notes below.
    1. The rules were created under ‘Windows Server 2003 Computer’, not enabled by default and overridden for the VC server (where the powershell script runs). You may want to target the rule differently, depending on your environment.
  6. Create a performance view, showing performance data collected by the two rules above.
  7. Create a monitor, using the LogicalDisk ‘% Disk Free’ object and counter to monitor on a threshold of 10%. Similarly, the monitor could be created under ‘Windows Server 2003 Computer’, not enabled by default and overridden for the VC server. The alert contains the following information:
    1. Object: $Data/Context/ObjectName$, Counter: $Data/Context/CounterName$, Instance: $Data/Context/InstanceName$, Value: $Data/Context/Value$

Note that when using the script performance data provider, the ‘Performance Mapper’ tab is used to map the data returned by the script to the database. To make this generic, both the instance name and the value are used from each element:

  • Object: LogicalDisk
  • Counter: Free Megabytes
  • Instance: $Data/Property/@Name$
  • Value: $Data/Property[@Name]$

Note that the VBScript creates three typed ‘property bags’ and returns all. This results in a single XML that contains three dataitem elements, one for each instance. Creating a single property bag and adding the three instances results in only the first being processed.

This should now gather free space in MB and as a percentage of the size for all VMFS datastores your VC server knows about, visible through the performance view, and alerted on when free space is less than 10% of the capacity for each volume.

SQL Queries

Some SQL queries against the OperationsManagerDW database I used along the way to query from the datawarehouse that the data was being gathered. Note that where I've specified 'datastore%' as a filter, you'll need to change this to the prefix of your datastores, eg ds01, ds02 would be ds%.

/*Return the raw performance data collected for the datastore* instances: */

select DateAdd(Hour, 10, DateTime) as DateTime, InstanceName, SampleValue from perf.vperfRaw
inner join vPerformanceRuleInstance on perf.vperfRaw.PerformanceRuleInstanceRowID = vPerformanceRuleInstance.PerformanceRuleInstanceRowID
where InstanceName like 'datastore%'
order by datetime desc

/* Find new rules: */
select top 10 * from dbo.vPerformanceRuleInstance
where instancename like 'datastore%'
order by performanceruleinstancerowid desc

/* Find new raw performance data: */

select top 100 DateTime, SampleValue, ObjectName, CounterName, FullName, Path, Name, DisplayName, ManagedEntityDefaultName from perf.vperfraw
inner join vPerformanceRule on perf.vperfraw.PerformanceRuleInstanceRowID = vPerformanceRule.RulerowID
inner join vManagedEntity on perf.vperfraw.ManagedEntityRowID = vManagedEntity.ManagedEntityRowID
order by datetime desc

/* Find new rule instances that have been created: */

select top 10 * from dbo.vPerformanceRuleInstance
order by performanceruleinstancerowid desc
--


#PowerShell Script - PerfGatherVMwareDSSpace.ps1
# Note that this includes a context to connect to VC, with hardcoded username and password.  You could avoid this by running the scheduled task under the security context you created with inherent rights to VC, and then remove the explicit -credential argument to Connect-VIServer.

$ErrorActionPreference = "SilentlyContinue"
add-pssnapin VMware.VimAutomation.Core

$ADMINLOG = "c:\admin\logs"
$outputFile = ""
$today = [DateTime]::Now.ToString("yyyyMMdd")

$scriptName = $MyInvocation.MyCommand.Name
$scriptName = $scriptname.substring(0, $scriptname.LastIndexOf("."))

if ($env:adminlog -ne $null) {        # Was there an adminlog environment variable?
    $outputFile = $env:adminlog
} else {
   $outputFile = $ADMINLOG        # No, use the constant default  
}

$outputFile += "\" + $scriptname + "_" + $today + ".csv"    # Construct the full path to the output file 

$server = "vc01"          # VC instance
$username = "domain\user"        # Hardcoding is bad, but at least this is a RO account.
$password = "password"

$pwd = convertto-securestring $password -asplaintext -force

$cred = New-Object Management.Automation.PSCredential ($username, $pwd)   # Create the credentials to use with the VC connection

$vmserver = & { trap {continue}; Connect-VIServer -server $server -Credential $cred } # Connect to VC, trapping the error

if ($vmserver -is [System.Object]) {       # Do we have a connection?
    Get-Datastore -server $vmserver | export-csv -noTypeInformation -path $outputFile # Yes, get the datastore details and store as CSV
    if (test-path -path $outputFile) {
        write-output "Datastore details exported to $outputFile"
    } else {
        write-output "Error: Datastore details were not exported to $outputFile"
    }
    $vmserver = $null
} else {
    write-output "Could not connect to $server"
}


#------



' VBScript


' Parse a CSV file containing the VMware volume information, and return the specified field from the data file or the calculated percent free

Const TOKEN_DATE = "%date%"
Dim SOURCE_FILE : SOURCE_FILE = "c:\admin\logs\PerfGatherVMwareDSSpace_" & TOKEN_DATE & ".csv"  ' Today's log file exported from the PowerShell VI-snapin script

Const DELIMITER = ","           ' CSV data file
Const ForReading = 1
Const DISKTYPE_VMFS = "VMFS"          ' We're interested only in lines with VMFS volumes

Const QUERY_SIZE = "Size"
Const QUERY_FREE = "Free"
QUERY_PERCENT = "Percent"

Dim QUERY_DEFAULT : QUERY_DEFAULT = QUERY_FREE 

Const FIELD_SIZE = 1           ' The field in the CSV that contains the capacity of the VMFS volume
Const FIELD_FREE = 0           ' The field in the CSV that contains the free disk space on the VMFS volume
Const FIELD_PERCENT = 2

Const PerfDataType  = 2

Set oFSO = CreateObject("Scripting.FileSystemObject")

Main()

wscript.quit(0)

Sub Main()

 If WScript.Arguments.UnNamed.Count >= 1 Then
  strQueryType = WScript.Arguments.UnNamed(0)
  wscript.echo "Command-line argument passed, querying for " & strQueryType
 Else
  strQueryType = QUERY_DEFAULT
  wscript.echo "No command-line argument passed, querying for the default of " & strQueryType
 End if

 Select Case strQueryType
  Case QUERY_SIZE  strField = FIELD_SIZE 
  Case QUERY_FREE  strField = FIELD_FREE
  Case QUERY_PERCENT strField = QUERY_PERCENT
  Case Else  strField = FIELD_FREE
 End Select

 dtmDate = Now 
 strToday = DatePart("yyyy", dtmDate)         ' YYYY
 strToday = strToday & String(2 - Len(DatePart("m", dtmDate)),"0") & DatePart("m", dtmDate) ' MM
 strToday = strToday & String(2 - Len(DatePart("d", dtmDate)),"0") & DatePart("d", dtmDate) ' DD

 strDataFile = Replace(SOURCE_FILE, TOKEN_DATE, strToday)     ' Build the path to the data file
 wscript.echo "Looking for " & strDataFile

 Dim oAPI, oBag
 Set oAPI = CreateObject("MOM.ScriptAPI")

 If oFSO.FileExists(strDataFile) Then        ' Does the data file exist for today?
  WScript.Echo "Log file found, continuing"
  Call ReadFile(strDataFile, strBuffer)       ' Yes, read the contents of the file into the buffer

  For Each strLine in Split(strBuffer, vbCRLF)      ' For each line
   arrEntry = Split(strLine, DELIMITER)      ' Split the line into an array on comma
   If UBound(arrEntry) = 5 Then       ' Did this line have a valid number of fields?
    If strComp(arrEntry(3), DISKTYPE_VMFS) = 0 Then    ' Yes, is it a VMFS volume? (excludes the header)
    wscript.echo "Processing " & strLine
     If IsNumeric(arrEntry(FIELD_FREE)) AND IsNumeric(arrEntry(FIELD_SIZE)) Then     ' Yes, is the field we're after a numeric?
      Set oBag = oAPI.CreateTypedPropertyBag(PerfDataType)  Create a typed name/value pair property bag

      If (strField = FIELD_SIZE) OR (strField = FIELD_FREE ) Then
       Call oBag.AddValue(arrEntry(5),CLng(arrEntry(strField))) ' Yes, convert to a long and add to the bag
      ElseIf strField = QUERY_PERCENT Then
       dblFreePercent = CDbl(arrEntry(FIELD_FREE) / arrEntry(FIELD_SIZE)*100)
       Call oBag.AddValue(arrEntry(5), Round(dblFreePercent, 2))
      End If
      oAPI.AddItem(oBag)     ' Add the item to the bag (doing it here results in three datatime elements)

     End If  
    End If
   End If
  Next

 Else
  WScript.Echo "Error: Daily data file not found - " & strDataFile 

 End If

 Call oAPI.ReturnItems 

End Sub    


'********************************************************
' Purpose: Read a file and store the contents in the buffer
' Assumptions: oFSO exists
'              strLogFile contains the path/filename of the file to operate on
' Effects: strBuffer is by reference
' Inputs: strFileName, Path and filename of the source file
'   strBuffer, the buffer used to store the contents of the file
' Returns: None
'
'********************************************************
Sub ReadFile (ByVal strFileName, ByRef strBuffer)   
 On Error Resume Next
 Dim objTextStream

 If Not oFSO.FileExists(strFileName) Then
  WScript.Echo "Error: " & strFileName & " file not found."
  Exit Sub
 End If
    Set objTextStream = oFSO.OpenTextFile(strFileName, ForReading)
 strBuffer = objTextStream.ReadAll
End Sub

'------

References:

How to Create a Probe-Based Performance Collection Rule in Operations Manager 2007
http://technet.microsoft.com/en-us/library/bb381406.aspx

MOMScriptAPI.CreatePropertyBag Method
http://msdn.microsoft.com/en-us/library/bb437556.aspx

XPath Examples:
http://msdn.microsoft.com/en-us/library/ms256086(VS.85).aspx

http://blogs.msdn.com/mariussutara/archive/2008/01/24/momscriptapi-createtypedpropertybag-method.aspx

http://blogs.technet.com/kevinholman/archive/2008/07/02/collecting-and-monitoring-information-from-wmi-as-performance-data.aspx

http://www.afinn.net/2008/07/collecting-performance-data-in-operations-manager-2007-and-publishing-to-sharepoint-part-1/

http://blogs.msdn.com/mariussutara/archive/2007/11/13/alert-description-and-parameter-replacement.aspx

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Saturday, November 1, 2008

Enumerating URLs in Internet Explorer

Have you ever wanted to get a list of the URLs you are currently browsing in Internet Explorer? Probably not, but occasionally I've got a large number of IE windows open and I realise I need to close them all, but I would like to know the pages I'm in the middle of browsing.

The PowerShell and VBScripts below do just that - enumerate the current iexplore.exe windows, and report the URL and name of any identified iexplore.exe windows.

This is also good for gathering references, if you've got many pages open and you'd like to record the URLs for later reference, I find this script easier than repeated copy/paste.



# PowerShell

$shell = new-object –com Shell.Application

$windows = $shell.Windows()

write-output ($windows.count.ToString() + " windows found")
foreach ($window in $windows) {
  if ($window.FullName -like "*iexplore*") {
    write-output ($window.LocationURL + ", " + $window.LocationName)
  }
}

$shell = $null



' VBScript
' Find the URLs of the currently running Internext Explorer Windows

' References:
'  http://windowssdk.msdn.microsoft.com/en-us/library/ms630310.aspx
'  http://msdn.microsoft.com/library/default.asp?url=/workshop/browser/webbrowser/reference/objects/internetexplorer.asp

Const IE_EXE = "iexplore.exe"

Call FindCurrentURLs(strURLSet)
WScript.Echo strURLSet

wscript.quit(0)

Function FindCurrentURLs(ByRef strURLSet)
 Dim objShell, objWindowSet, objWindow
 Dim strwindowName, strURL, strFullName

 Set objShell = CreateObject("Shell.Application")    ' Create a Windows shell automation object
 Set objWindowSet = objShell.Windows      ' Get the collection of open windows belonging to the shell

 Wscript.Echo "Processing " & objWindowSet.Count & " windows"   ' Report how many instances were found

 For Each objWindow in objWindowSet      ' For each InternetExplorer object in the ShellWindows set
  strFullName = objWindow.FullName     ' Get the full path and executable of this window
  If InStr(1, strFullName, IE_EXE, 1) <> 0 Then    ' Is this an IE shell object?
   strURL = objWindow.LocationURL     ' Get the URL

   If strURL <> "" Then 
    strURLSet = strURLSet & vbCRLF & strURL   ' Append to the set of URLs
   End If
  Else         ' No, probably explorer.exe skip
   WScript.Echo "Skipped " & strFullName & " - not IE"
  End If
 Next

 If Len(strURLSet) >= Len(vbCRLF) Then strURLSet = Right(strURLSet, Len(strURLSet) - Len(vbCRLF)) ' Strip the leading vbCRLF
 
 Set objShell = Nothing
 Set objWindowSet = Nothing : Set objWindow = Nothing
End Function

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Sunday, September 28, 2008

Reading Shortcuts with PowerShell and VBS

This post provides a simple method of enumerating shortcuts and retrieving their properties, either using PowerShell or VBScript. There is nothing particularly clever here, but I had a need a while back to use the enumeration, and I wrote the simple powershell equivalent to see how easy it would be. Note that I couldn't easily see a managed code .Net method to process shortcuts, so I fell back on the wscript COM object. More than likely there is a better method (get it? :) than this.

Run the scripts:


powershell . .\ReadShortcut.ps1 -p \\%server%\%share%\folder\path
powershell . .\ReadShortcut.ps1 -f \\%server%\%share%\shortcut.lnk
cscript //nologo "ReadShortcut.vbs" \\%server%\%share%\folder\path
cscript //nologo "ReadShortcut.vbs" \\%server%\%share%\shortcut.lnk

#
param(
   [string] $path = "",
   [string] $file = ""
   )

$WshShell = new-object -comobject "WScript.Shell"       # Instantiate the wscript.shell COM object

if ($path -ne "") {
    $shortcuts = get-childitem -path $path -filter "*.lnk" -rec    # Find all .lnk files, recursing in to subdirectories
    $shortcuts | foreach-object {$WshShell.CreateShortcut($_.FullName) }  # For each file, pass the fullname to the COM object to open the shortcut and enumerate the properties
} elseif ($file -ne "") {
    $shortcut = get-item -path $file       # Get the single file
    if ($shortcut -ne $null) { $WshShell.CreateShortcut($shortcut) }   # If exists, read the shortcut properties
} else {
    write-output "No arguments specified, please use either -p to specify a path or -f for a specific .lnk file"
}

#-------#


'ReadShortcut.vbs
' Read a shortcut or a top-level directory of shortcuts and write the properties to stdout
If WScript.Arguments.UnNamed.Count = 1 Then
 strShortcut = WScript.Arguments.UnNamed(0)
Else
 WScript.Echo "Please supply the name of an lnk file or directory to read, eg c:\test.lnk or c:\shortcuts"
 WScript.Quit(1)
End If

Set objFSO = CreateObject("Scripting.FileSystemObject")

If objFSO.FolderExists(strShortCut) Then      ' Was a directory specified?
 Set objFolder = objFSO.getFolder(strShortcut)     ' Get the folder
 
 For Each objfile in objFolder.Files      ' For each file in the top-level directory
  If objfile.type = "Shortcut" Then     ' Is this file a shortcut?
   Call Readshortcut(objFile.Path, strProperties)   ' yes, read the properties
   dtmCreationDate = objFile.DateCreated
   WScript.Echo dtmCreationDate & "," & strProperties  ' output the results
  End If 
 Next
ElseIf objFSO.FileExists(strShortCut) Then      ' Was an individual file specified?
 Call Readshortcut(strShortcut, strProperties)     ' read the properties of the file
 WScript.Echo strProperties       ' output the results
Else           ' file-not-found
 WScript.Echo "Error: Could not read '" & strShortcut & "'"
 WScript.Quit(2)
End If
Set objFSO = Nothing

Function Readshortcut(ByRef strShortcut, ByRef strProperties)

 set objWshShell = WScript.CreateObject("WScript.Shell")    ' Create the shell object
 set objShellLink = objWshShell.CreateShortcut(strShortcut)   ' Execute the createshortcut method, which also retrieves an existing shortcut
 strProperties = strShortCut & "," & objShellLink.TargetPath & "," & objShellLink.WindowStyle & "," & objShellLink.Hotkey & "," & objShellLink.IconLocation & "," & objShellLink.Description & "," & objShellLink.Arguments & "," & objShellLink.FullName & "," & objShellLink.WorkingDirectory & """"

 ' This propertly can be set, but not read? - objShellLink.RelativePath
 Set objShellLink = Nothing
 Set objWshshell = Nothing
End Function

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Sunday, September 7, 2008

Creating secedit templates with PowerShell

This post provides a powershell script to create a secedit security template based on an existing NTFS filesystem permissions structure. This script uses the PowerShell get-childitem cmdlet combined with the get-acl cmdlet to provide the SDDL string, which is then processed to print out only explicit ACLs, after stripping out inherited ACE's in a very cheap and nasty regular expression matching way.

Using this script provides a basic DACL per-directory secedit template type view of a filesystem, excellent to move away from directly applying ACLs to the filesystem or just to provide point-in-time views of your NTFS security.

How good is powershell?


# -- CreateSecurityTemplate.ps1 -- #
#
# 06/09/2008, Wayne Martin, Initial version
#
#
# Description:
#   Given a starting directory, recursively list explicit ACLs in SDDL format for reproduction in a secedit security template
#
# Assumptions, this script works on the assumption that:
#   Only discretionary access control entries are used
#
# Limitations:
#   260 max_path length limitation is in place with get-childitem
#
# Arguments:
#  -p : Path     - The root folder to begin the search
#
# Example:
#   . .\CreateSecurityTemplate.ps1 -p c:\windows\temp

param ($path = "")

if ($path -eq "") {
    write-output "Please specify a root directory to begin the search, eg . .\CreateSecurityTemplate.ps1 -p c:\windows\temp"
    exit 2
} else {
    write-output "Processing $path"
}

$ErrorActionPreference = "SilentlyContinue"

$EXPLICIT_ACL_OVERWRITE = 2
$EXPLICIT_ACL_MERGE = 2

$PATTERN_SPLIT_ACL = "^\(|\)\(|\)$"
$PATTERN_NOT_INHERITED_ACE = ".;.*ID.*;"
$PATTERN_EMPTY_LINE = "^$"

$DALC_AUTOINHERIT_REQ = "D:AR"
$path

$objects = $null
$objects = get-childitem $path -Recurse | where{$_.PSIsContainer}    # Find directories

foreach ($object in $objects)          # For each directory
{
    if ($object -is [System.IO.DirectoryInfo])
    {
        $FullName = $object.FullName
        $acl = get-acl -path $FullName        # Get the ACL for this directory

        $sddl = $acl.sddl         # Get the ACL in SDDL string format
        $sddl = $sddl.remove(0, $sddl.indexof("("))
 
 # Split to each ACE, return only those that are not inherited and not an empty line
        $aces = [regex]::split($sddl,$PATTERN_SPLIT_ACL) | where{ $_ -notmatch $PATTERN_NOT_INHERITED_ACE } | where{ $_ -notmatch $PATTERN_EMPTY_LINE} 

        if ($aces.length -gt 1) {        # Are there any explicit aces on this directory?
            $newSDDL = "(" + [string]::join(")(", $aces) + ")"     # Yes, construct the new SDDL string
            write-output ("""" + $FullName + """,$EXPLICIT_ACL_OVERWRITE,""$DALC_AUTOINHERIT_REQ" + $newsddl + """")
        }
    }
}

exit 0

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Sunday, August 31, 2008

Fixing Permissions with NTFS intra-volume moves

This post discusses methods to automatically correct permission problems associated with moving data within a single NTFS volume in NTFS5.x - Windows 2000 and 2003 (and XP). Data secured with different ACLs on a single volume that is moved will normally result in incorrect permissions, as the data is re-linked in the MFT without taking into account permission inheritance.

This problem will occur if:

  1. The user context that initiated the move - either locally or through a share - has the delete permission to the root directory object being moved and the right to create in the new location
  2. The target location does not already contain a folder with the same name (if the folder does exist a copy/delete is performed rather than a move).
For example:
 

\\Server\Share\Folder1   - localA:C
\\Server\Share\Folder1\A - localA:C inherited from the root
\\Server\Share\Folder2   - localB:C
\\Server\Share\Folder2\B - localB:C inherited from the root


UserAB who has access to both Folder1 and Folder2, performs a drag and drop operation in explorer, with the source of Folder2\B and a drop-target of Folder1.

After the move, the permissions on \\Server\Share\Folder1\B are still inherited with access to localB, and no access to localA.

How to fix the problem

This can be fixed by using setacl or icacls to reset permission inheritance, or by using security templates to control permissions to the filesystem.

setacl

Reset permission inheritance:
setacl -on %Directory%\*.* -ot file -actn rstchldrn -rst DACL

setacl.exe is a very powerful permissions utility for reporting and modifying ACLs.

In the example above, to reset permissions inheritance for each folder:
for /d %i in (\\server\share\*) do echo setacl -on %i\*.* -ot file -actn rstchldrn -rst DACL

icacls

Reset permission inheritance:
icacls %Directory% /reset /T /C

In the example above, to reset permissions inheritance for each folder:
for /d %i in (\\server\share\*) do echo icacls %i /reset /T /C

icacls is a 2003 SP2 utility, but also runs on XP.

Security Templates

I find that security templates are an excellent method of managing permissions, as they provide:

  • A repeatable method of applying permissions, great for fixing mistakes, DR, restore
  • Accountability and change control - it's easy to see who made changes to a security template, and with templates rollback and change control is much easier
  • Auditing - It's very simple to provide the results of the template to auditors showing your security structure

To reapply the security template, you could run (prefix with psexec to run remotely):
secedit /configure /db c:\windows\temp\%random%.sdb /cfg c:\windows\security\templates\ExampleTemplate.inf /log c:\windows\temp\example.log

Note that for this to reset inheritance, each security template entry must use 2 in the second field, which directs secedit to overwrite existing explicit ACEs, a by-product of which is that inherited ACLs are reset on child objects. If you use a second column of 0 - to merge the results, the incorrectly set inherited ACL is not reset on the child objects.

If you had a security template managing permissions to the example above, it would look something like:

 

[Unicode]
Unicode=yes
[Version]
signature="$CHICAGO$"
Revision=1

[Profile Description]
Description=Example Template

[File Security]
;Set security for Folder1
"D:\Share\Folder1",2,"D:AR(A;OICI;FA;;;BA)(A;OICI;0x1301bf;;;S-1-5-21-129063155-272689390-804422213-3709)(A;OICI;FA;;;SY)"
"D:\Share\Folder2",2,"D:AR(A;OICI;FA;;;BA)(A;OICI;0x1301bf;;;S-1-5-21-129063155-272689390-804422213-3710)(A;OICI;FA;;;SY)"



How to identify the problem

Below is a rather inefficient and simple PowerShell script that will report directories that have inherited ACLs that don't match the parent directory. I'm sure there are better ways to do this, but secedit /analyze doesn't do it and while I started off parsing cacls /S and setacl output with a VBScript, I think the PowerShell script is at least better than that. It works only for simple permission structures, ie you’ve set permissions at the root of somewhere and expecting them to inherit all the way to the end.

I say the script is quite inefficient in that even though I'm filtering the output of get-childitem in the resulting array to return only directories, I believe it still processes all files and directories. And then for each directory I'm finding the parent and checking the ACLs - where it would be more efficient to find the parent and then process all directories directly under the parent before recursing.

Anyway, once you've found the directories, you can often use 'dir /q' to report the new owner, which in testing I've done is set at the person doing the move on the new root folder object.

Note that these permissions problems can occur with files, but the script below only checks directories (because it seemed overkill to check each file when there could be millions, plus it's quite plausible that directory ACLs don't match file ACLs).

Output based on the example above:
PS C:> . .\CheckInheritedSecurity.ps1 -p D:\Share
The ACE for 'TEST\wm' on 'D:\Share\Folder1\B' is marked as inherited but doesn't appear to have been inherited directly from the parent directory

 

$root = ""

if ($args.count -eq 2) {
  for ($i = 0; $i -le $args.count-1; $i+=2) {
    if ($args[$i].ToLower().Contains("-p")) {
      $root = $args[$i+1]
    }
  }
}

if ($root -eq "") {
  write-output "Please specify a root directory to begin the search"
  exit 2
}

$rootSubDirs = get-childitem $root  where{$_.PSIsContainer}

foreach ($tld in $rootSubDirs)
{
  $objects = $null
  $objects = get-childitem $tld.FullName -Recurse  where{$_.PSIsContainer}

  foreach ($object in $objects)
  {
    if ($object -is [System.IO.DirectoryInfo])
    {
      $FullName = $object.FullName
      $acl = get-acl -path $FullName
      $accessRules = $acl.GetAccessRules($false, $true, [System.Security.Principal.NTAccount])      # Report only inherited, as NTAccount (not SIDs)

      $parent = $object.Parent
      $parentFullName = $parent.FullName
      $parentacl = get-acl -path $parent.FullName

      $ParentAccessRules = $parentacl.GetAccessRules($true, $true, [System.Security.Principal.NTAccount])      # Report explicit and inherited, as NTAccount (not SIDs)

      #write-output ($object.fullname + ", child of " + $parent.FullName)

      foreach ($accessRule in $accessRules)
      {
        $InheritedFromParent = $false

        foreach ($parentAccessRule in $ParentAccessRules)
        {
          if ($accessRule.IdentityReference -eq $parentAccessRule.IdentityReference) { $InheritedFromParent = $true }
        }

        if (!$InheritedFromParent)
        {
          $identity = $AccessRule.IdentityReference.ToString()
          write-output ("The ACE for '$identity' on '$FullName' is marked as inherited but doesn't appear to have been inherited directly from the parent directory")
        }
   
      }
    }
  }
}

exit 0


References:

SetACL
http://setacl.sourceforge.net/

SDDL syntax in secedit security templates
http://waynes-world-it.blogspot.com/2008/03/sddl-syntax-in-secedit-security.html

Create or modify a security template for NTFS permissions
http://waynes-world-it.blogspot.com/2008/03/create-or-modify-security-template-for.html

Useful NTFS and security command-line operations
http://waynes-world-it.blogspot.com/2008/06/useful-ntfs-and-security-command-line.html

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Friday, August 29, 2008

Converting filetime with vbs and PowerShell

This post provides two methods of converting the 64-bit Windows filetime structure - the Windows Epoch giving the number of 100 nanosecond intervals since 01/01/1601 UTC.

The first is PowerShell which is relatively easy, compared to the second which is some VBScript that I have used upon occasion, typically when I'm trying to convert 64-bit integers from AD (eg returned from dsquery pwdlastset), or vice versa.

Neither of these scripts are revolutionary, but I haven't come across a simple function to convert between the two in vbs, and I thought I'd include the powershell for comparison.


# ConvertFileTime.ps1
$now = [datetime]::Now
$now

$fileTime = $now.ToFileTime()
$fileTime 
[datetime]::FromFileTime($fileTime)

# This then parses the date, determining whether it is a valid date or not (in this case it always will be beacuse it's from datetime, but you could use this to parse other date strings, using your culture)
foreach ($date in [string[]]$now.ToString()) {
  write-output $date
  $oCulture= [System.Globalization.CultureInfo]"en-AU"
  $dtOut = new-object DateTime
  [datetime]::TryParse($date, $oCulture, [System.Globalization.DateTimeStyles]::None, [ref]$dtOut)
  [datetime]::TryParse($date, [ref]$dtOut)
}

-

' ConvertFileTime.vbs
' VBScript doesn't support 64-bit integers, so it can't handle the number of 100 nanosecond intervals since 01/01/1601
' http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnclinic/html/scripting09102002.asp

' Either use ADSI provider and the IADs/IADsLargeInteger object
' LargeIntValue = objLargeInt.HighPart * 2^32 + objLargeInt.LowPart
' http://msdn.microsoft.com/library/default.asp?url=/library/en-us/adsi/adsi/iadslargeinteger.asp'

' Or WMI, which handles the conversion between 64-bit datetime structure / UTC / and VB var datetime

If Wscript.Arguments.UnNamed.Count > 0 Then 
 strDateTime = Wscript.Arguments.UnNamed(0)

 Set objDateTime = CreateObject("WbemScripting.SWbemDateTime")

 If IsDate(strDateTime) Then
  Call objDateTime.SetVarDate(strDateTime, False)
  wscript.echo objDateTime.GetFileTime
 Else
  Call objDateTime.SetFileTime(strDateTime, False)
  wscript.echo objDateTime.GetVarDate
 End If

 intReturn = 0
Else
 WScript.Echo "Specify a filetime or a date to convert, eg 127076450620627215, or ""11/04/2006 11:17:10 AM"""
 intReturn = 2
End If

WScript.Quit(intReturn)

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Wednesday, August 27, 2008

Difference between bat and cmd

I've occasionally thought on the difference between cmd and bat, and from an execution point of view I didn’t think there was any. But it turns out a few commands can modify the execution path of batch files, because they modify the errorlevel differently based on whether they were executed as a .bat or .cmd.

The comment that I did find from Microsoft (the source was MZ according to the signature block!)

The differences between .CMD and .BAT as far as CMD.EXE is concerned are: With extensions enabled, PATH/APPEND/PROMPT/SET/ASSOC in .CMD files will set
ERRORLEVEL regardless of error. .BAT sets ERRORLEVEL only on errors.

If you save the text below as test.bat and test.cmd, and then run each from a command prompt, you see two different results. Note that command extensions are enabled by default on XP, a requirement for this behavioural difference.

I saw several references to bat running under 16-bit VDM and cmd running under 32-bit when executed from a shortcut, however I couldn’t reproduce this on XP SP2.

In addition, apparently 9x days and before there was only bat? And then with NT CMD was introduced, and running the same .bat file on 9x and NT definitely had different results, so having two different extensions made it less likely to accidently run a cmd written for NT on a 9x box if you were interoperating between the two. This sounds plausible, but I can't remember those sorts of details that far back.

I hope this doesn’t excite anyone.


::
:: When called from a cmd, 'set' resets errorlevel, whereas when called from a bat the errorlevel from the previous command is returned.
:: The four examples below show this in different ways, two calling a subroutine, and two using a single line.
::
:: :test generates an error by find without any parameters (2)
:: The set command then clears errorlevel when run as a .cmd file, but when run as a .bat, the previous errorlevel is returned
:: 
:: Delayed environment variable expansion is enabled to allow showing ERRORLEVEL, but the same result occurrs if you use don't use delayed expansion (you just can't see the errorlevel)
:: Therefore the single line below with delayed expansion shows the difference in return errorlevel, but the call to the label allows the if..then..else to also show the same results
::
:: The last example uses concatenated commands, with the final command checking errorlevel and using if..else to report the difference
::
:: && - If
:: || - Else
::
:: Comment from msft:
:: The differences between .CMD and .BAT as far as CMD.EXE is concerned are: With extensions enabled, PATH/APPEND/PROMPT/SET/ASSOC in .CMD files will set ERRORLEVEL regardless of error. .BAT sets ERRORLEVEL only on errors. 


@echo off
::setlocal ENABLEEXTENSIONS
setlocal ENABLEDELAYEDEXPANSION

:: Call test, if successful echo cmd, else echo bat
call :test && echo cmd || echo bat

:: Call test, if successful echo cmd with delayed expansion of errorlevel, else echo bat
call :test && echo cmd error: !errorlevel! || echo bat error: !errorlevel! 

:: Generate the error, clear the error, echo the delayed expansion of errorlevel
find 2>nul & set test=test && echo Single-line error: !errorlevel!

:: Generate the error, clear the error, if errorlevel 1 echo BAT, else echo CMD
find 2>nul & set test=test & if errorlevel 1 (echo Run from BAT error) else (echo Run from CMD no error)

goto :EOF

:test

 :: Generate an error
 find 2>nul

 echo      :test %errorlevel%

 :: Set a variable - when called with .cmd the errorlevel is reset
 set test=test
 echo      :test %errorlevel%
::


Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Saturday, August 9, 2008

Renaming a user account in AD

This post contains a few methods to rename an account in Active Directory - the end result is moving the account to the same container with a new name, and then typically updating other attributes such as the sAMAccountName.

The same results can generally be achieved serveral ways:

  1. Use the 'dsmove -newname' command, and possibly the 'dsmod user -upn' command
  2. Run the VBScript below
  3. Use ldifde to modify the relevant attributes
  4. use dsa.msc to rename the account through the GUI

Notes:

  1. This does not modify the mailNickname, the userPrincipalName or the primary or proxy e-mail attributes, which you may also want to do as part of renaming an account.
  2. The RDN attribute has the LDAP display name of 'Name', automatically updated when you modify the CN/DN of an object


' -- RenameAccount.vbs -- '
If WScript.Arguments.UnNamed.Count = 3 Then
 sOU = WScript.Arguments.UnNamed(0)
 sExistingCN = WScript.Arguments.UnNamed(1)
 sNewCN = WScript.Arguments.UnNamed(2)
Else
 WScript.Echo "Please supply an OU, and the old and new CN, eg RenameAccount.vbs ""CN=Users,DC=domain,DC=com"" AccountOld AccountNew"
 WScript.Quit(0)
End If

If sExistingCN = "" OR sOU = "" OR sNewCN = "" Then
 wscript.quit(2)
Else
 wscript.echo "Moving " & "LDAP://cn=" & sExistingCN & "," & sOU & ", to " & sNewCN
End If


Set objOU = GetObject("LDAP://" & sOU)
objOU.MoveHere "LDAP://cn=" & sExistingCN & "," & sOU, "cn=" & sNewCN   ' Rename the account

sUserADsPath = "LDAP://cn=" & sNewCN & "," & sOU
Set oUser = GetObject(sUserADsPath)       ' Get the newly renamed object

wscript.echo "Current SAM account name: " & oUser.sAMAccountName
oUser.sAMAccountName = sNewCN        ' Update the sAMAccountName attribute
oUser.SetInfo          ' Write the object
wscript.echo "New SAM account name: " & oUser.sAMAccountName

-

References: RDN attribute on MSDN: http://msdn.microsoft.com/en-us/library/ms678697(VS.85).aspx
Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Sunday, July 13, 2008

Process list with command-line arguments

This post provides information on using WMI to provide a list of processes with additional information, including the full path to the executable and any parameters passed to the command-line. While this may not sound that useful, this can greatly assist when troubleshooting or just understanding how applications work.

You can run this command against one server, or specify multiple nodes with a control file:
wmic /node:"server01" path win32_process get ExecutablePath,Caption,CommandLine,CreationDate,WorkingSetSize,ProcessId

To filter the list, you could also add a where clause:
wmic /node:"server01" path win32_process Where "Caption Like '%cscript%'" get ExecutablePath,Caption,CommandLine,CreationDate,WorkingSetSize,ProcessId

This information can be useful for diagnosing processes:

For example, Operations Manager 2007 uses cscript quite heavily, and at times I've seen many cscript processes running, but had no clear idea what they were doing.


cscript.exe "C:\WINDOWS\system32\cscript.exe" /nologo "CPUUtilization.vbs" 95 15 opsmgr01.test.local 100. 20080713130117.079481+600 9876 5820416
cscript.exe "C:\WINDOWS\system32\cscript.exe" /nologo "MemoryUtilization.vbs" 2.5 opsmgr01.test.local 114.66666666666667 20080713130301.876356+600 6832 2473984
cscript.exe "C:\WINDOWS\system32\cscript.exe" /nologo "DiscoverHealthServiceCommunicationRelationships.js" 20080713130337.876356+600 8484 2457600

Processes that normally show up as just 'cmd.exe' with tools like pslist.exe can easily be further identified:

cmd.exe CMD /D /S /Q /C""C:\Program Files (x86)\VERITAS\VxPBX\bin\pbxservice.cmd" "C:\Program Files (x86)\VERITAS\VxPBX\bin\pbx_exchange.exe""

It's easy to see command-line parameters used to launch some applications, eg, the 'manage your server' wizard is started with:
C:\WINDOWS\system32\oobechk.exe /LaunchMYS

And screensavers are started with a /s parameter:
logon.scr logon.scr /s

It is easy to see which host groups are being run by which instance of svchost (tasklist /svc also shows this information):

svchost.exe C:\WINDOWS\system32\svchost.exe -k DcomLaunch 20080323192353.500000+600 C:\WINDOWS\system32\svchost.exe 676 5496832
svchost.exe C:\WINDOWS\system32\svchost.exe -k rpcss 20080323192354.187500+600 C:\WINDOWS\system32\svchost.exe 780 9392128
svchost.exe C:\WINDOWS\system32\svchost.exe -k NetworkService 20080323192402.828125+600 C:\WINDOWS\system32\svchost.exe 1016 7897088
svchost.exe C:\WINDOWS\system32\svchost.exe -k LocalService 20080323192402.828125+600 C:\WINDOWS\system32\svchost.exe 1036 5959680
svchost.exe C:\WINDOWS\System32\svchost.exe -k netsvcs

Inconsistencies show up, such as instances of a cluster resource monitor on an x64 server, some running native, some WOW64:
resrcmon.exe "C:\WINDOWS\SysWOW64\resrcmon.exe" -e 1464 -m 1468 -p 2744 20080323192505.936883+600 C:\WINDOWS\SysWOW64\resrcmon.exe 3652 5472256
ResrcMon.exe "C:\WINDOWS\cluster\resrcmon.exe" -e 1592 -m 1596 -p 2744 20080323192506.686004+600 C:\WINDOWS\cluster\resrcmon.exe 3716 8388608

Instances of rundll32 and similar launch methods, often showing up interesting things, eg a notification baloon launched through rundll32:
rundll32.exe RunDll32.exe wlnotify.dll,ShowNotificationBalloon Global\00000000f0357177_WlballoonKerberosNotificationEventName

Executing this command

Add as a doskey macro

Put the following line into a text file called macros.txt:
PSL=if "$1" EQU "" (wmic path win32_process get ExecutablePath,Caption,CommandLine,CreationDate,WorkingSetSize,ProcessId) else (wmic /node:"$1" path win32_process get ExecutablePath,Caption,CommandLine,CreationDate,WorkingSetSize,ProcessId)

Then run the following command, which will execute the doskey command to install the macro as a command prompt is started:
reg add "hklm\software\microsoft\command processor" /v AutoRun /t reg_sz /d "doskey /macrofile=%path%\macros.txt"

The command works either locally or with a parameter, so you can either run:
psl
psl server01


Call through Scripting

Instead of using wmic, you could also use scripting - either powershell or vbscript - to query the WMI instances.

eg, in PowerShell:

Get-WmiObject win32_process  Format-Table ExecutablePath,Caption,CommandLine,CreationDate,WorkingSetSize,ProcessId

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Sunday, June 15, 2008

PowerShell Low-level keyboard hook

This post provides an example PowerShell script to create a global system low-level keyboard hook, detecting and discarding the first Alt+Tab combination after the hook is installed. This was developed and tested on Windows Vista.

This is run out of a PowerShell script using runtime compiled VB.Net code, with a form-less Application.Run() call to wait for the message. You could also hook WH_MOUSE_LL using similar code.

I was originally trying to hook WH_GETMESSAGE, but it seems this is not possible without a DLL which is loaded and injected into running processes to provide the shared address space, something which negated what I was trying to achieve (a simple demo with PowerShell).

Note that the MSDN SetWindowsHookEx Function description (http://msdn.microsoft.com/en-us/library/ms644990.aspx) defines WH_KEYBOARD_LL as a global only hook, which to my understanding means it needs to be referenced in a DLL, which is most definitely not the case below as hMod is intptr.zero and it's all running out of script/dynamic code. I also tested using the hinstance of the compiled module, which didn't work, and using loadlibrary user32 which did work (I don't understand why I just copied from some other example).

 

## KeyboardHookExample.ps1 ##

#
# Description:
#  Example global WH_KEYBOARD_LL hook in PowerShell using VB.Net runtime compiled code.
#  Adds a hook in the LIFO chain for all keyboard activity to CallbackFunction(), which checks for and discards the first Alt+Tab combination
#
# Author: 
#  Wayne Martin, 15/06/2008, http://waynes-world-it.blogspot.com/
#
# References:
#  CLRInsideOut\WinSigGen.exe
#  http://msdn.microsoft.com/en-us/library/ms644959(VS.85).aspx
#  http://msdn.microsoft.com/en-us/library/ms644960(VS.85).aspx
#  http://support.microsoft.com/kb/319524
#  http://blogs.msdn.com/toub/archive/2006/05/03/589468.aspx


$provider = new-object Microsoft.VisualBasic.VBCodeProvider
$params = new-object System.CodeDom.Compiler.CompilerParameters
$params.GenerateInMemory = $True
$refs = "System.dll","Microsoft.VisualBasic.dll","System.Windows.Forms.dll"
$params.ReferencedAssemblies.AddRange($refs)

# VB.NET EXAMPLE
$txtCode = @'

Imports Microsoft.VisualBasic
Imports System
Imports System.Runtime.InteropServices
Imports System.Windows.Forms

Public Class hookExample
    Public hookHandle As Integer
    Public hWndExplorer as Integer

     Private CallbackDelegate As HookProc

      _
    Public Structure KBDLLHOOKSTRUCT
        '''DWORD->unsigned int
        Public vkCode As UInteger
        '''DWORD->unsigned int
        Public scanCode As UInteger
        '''DWORD->unsigned int
        Public flags As UInteger
        '''DWORD->unsigned int
        Public time As UInteger
        '''ULONG_PTR->unsigned int
        Public dwExtraInfo As UInteger
    End Structure

    '''Return Type: LRESULT->LONG_PTR->int
    '''code: int
    '''wParam: WPARAM->UINT_PTR->unsigned int
    '''lParam: LPARAM->LONG_PTR->int
      _
    Public Delegate Function HOOKPROC(ByVal code As Integer, ByVal wParam As System.IntPtr, ByVal lParam As System.IntPtr) As Integer

      _
    Public Structure HHOOK__
        '''int
        Public unused As Integer
    End Structure

      _
    Public Structure HINSTANCE__
        '''int
        Public unused As Integer
    End Structure

    '''Return Type: HHOOK->HHOOK__*
    '''idHook: int
    '''lpfn: HOOKPROC
    '''hmod: HINSTANCE->HINSTANCE__*
    '''dwThreadId: DWORD->unsigned int
      _
    Public Shared Function SetWindowsHookExW(ByVal idHook As Integer, ByVal lpfn As HOOKPROC,  ByVal hmod As System.IntPtr, ByVal dwThreadId As UInteger) As System.IntPtr
    End Function

    '''Return Type: LRESULT->LONG_PTR->int
    '''hhk: HHOOK->HHOOK__*
    '''nCode: int
    '''wParam: WPARAM->UINT_PTR->unsigned int
    '''lParam: LPARAM->LONG_PTR->int
      _
    Public Shared Function CallNextHookEx( ByVal hhk As System.IntPtr, ByVal nCode As Integer, ByVal wParam As System.IntPtr, ByVal lParam As System.IntPtr) As Integer
    End Function

    '''Return Type: BOOL->int
    '''hhk: HHOOK->HHOOK__*
      _
    Public Shared Function UnhookWindowsHookEx( ByVal hhk As System.IntPtr) As  Boolean
    End Function

    '''HC_ACTION -> 0
    Public Const HC_ACTION As Integer = 0

    '''LLKHF_ALTDOWN -> (KF_ALTDOWN >> 8)
    Public Const LLKHF_ALTDOWN As Integer = (KF_ALTDOWN) >> (8)
    
    '''KF_ALTDOWN -> 0x2000
    Public Const KF_ALTDOWN As Integer = 8192

    '''VK_TAB -> 0x09
    Public Const VK_TAB As Integer = 9

    '''WH_KEYBOARD_LL -> 13
    Public Const WH_KEYBOARD_LL As Integer = 13

    '''Return Type: HMODULE->HINSTANCE->HINSTANCE__*
    '''lpLibFileName: LPCWSTR->WCHAR*
      _
    Public Shared Function LoadLibraryW( ByVal lpLibFileName As String) As System.IntPtr
    End Function


    Public Sub New()
        CallbackDelegate = New HookProc(AddressOf CallbackFunction)

        'Dim hInstance As IntPtr
        ' This doesn't work:
        'hInstance = Marshal.GetHINSTANCE(Reflection.Assembly.GetExecutingAssembly().GetModules()(0))     
        'hookHandle = SetWindowsHookExW(WH_KEYBOARD_LL, CallbackDelegate, hInstance, 0)

        ' This does work, but is unnecessary and intptr.zero also works:
        'hInstance = LoadLibraryW("User32")
        'hookHandle = SetWindowsHookExW(WH_KEYBOARD_LL, CallbackDelegate, hInstance, 0)

       hookHandle = SetWindowsHookExW(WH_KEYBOARD_LL, CallbackDelegate, IntPtr.Zero, 0)
        If (hookHandle > 0) Then
            console.writeline("Keyboard hooked, press Alt+Tab when any window has focus to test")
        End If
        Application.Run()  
    End Sub


    Public Function CallbackFunction(ByVal code As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
        Dim HookStruct As KBDLLHOOKSTRUCT
        If (code < 0) Then
            Return CallNextHookEx(hookHandle, code, wParam, lParam)
        End If

        hookStruct = CType(Marshal.PtrToStructure(lParam, GetType(KBDLLHOOKSTRUCT)),KBDLLHOOKSTRUCT)

        If (Hookstruct.vkCode = VK_TAB) And (Hookstruct.flags And LLKHF_ALTDOWN) Then
            console.writeline("Alt+Tab detected, discarding and removing hook")
            Call UnhookWindowsHookEx(hookHandle)
            Application.Exit()
            Return 1
        Else
            Return CallNextHookEx(hookHandle, code, wParam, lParam)
        End If
    End Function

End Class

'@

$results = $provider.CompileAssemblyFromSource($params, $txtCode)

if ($results.Errors.Count) {
    $codeLines = $txtCode.Split("`n");
    foreach ($ce in $results.Errors)
    {
        write-host "Error: $($codeLines[$($ce.Line - 1)])"
        write-host $ce
        #$ce out-default
    }
    Throw "INVALID DATA: Errors encountered while compiling code"
 }

$mAssembly = $results.CompiledAssembly
$i = $mAssembly.CreateInstance("hookExample")
#$r = $i.New()


Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Wednesday, June 4, 2008

VMware SDK with Powershell

This post provides a script using the VMware SDK with PowerShell to access a VI3 VirtualCenter server and enumerate the VMs. It's really just a proof of concept of what's possible, and a simple example of how to use the rather convoluted (in my opinion) VMware SDK.

Note that VMware have a PowerShell snap-in that makes everything VMware-related *much* easier, but there will always be lower-level tasks that the SDK is useful for.



# VMware SDK automation.
#
# Changes:
#  30/01/2008, Wayne Martin, Initial version
# 
# References: 
#  http://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.vm.GuestInfo.NicInfo.html
#
# Note that you need the compiled VMware SDK VimService.dll for this to work, and I'm using http rather than SSL - you may have to modify your vpxd.cfg 

$vmPath = ".\VimService.dll"
[Reflection.Assembly]::LoadFrom("$vmPath")

$serviceURL = "http://VCServer/sdk"
$usernameSDK = "domain\user"
$passwordSDK = "password"
$localeSDK = "en_au"

$pathDatacenter = "/DATACENTER"
$pathVMFolder = $pathDatacenter + "/vm"

$svcRef = new-object ManagedObjectReference
$svcRef.type = "ServiceInstance"
$svcRef.Value = "ServiceInstance"

$service = new-object VimService
$service.Url = $serviceURL
$service.CookieContainer = new-object System.Net.CookieContainer

$svcContent = $service.RetrieveServiceContent($svcRef)
$propCollectorRef = $svcContent.PropertyCollector

$service.Login($svcContent.sessionManager, $usernameSDK, $passwordSDK, $localeSDK)
$rootFolder = $svcContent.rootFolder

$dc2f = new-object TraversalSpec
$dc2f.type = "Datacenter"
$dc2f.path = "vmFolder"
$dc2f.skip = $false
$dc2f.selectSet = new-object SelectionSpec
$dc2f.selectSet[0].name = "traverseChild"
 
$traversalSpec = new-object TraversalSpec
$traversalSpec.type = "Folder"
$traversalSpec.name = "traverseChild"
$traversalSpec.path = "childEntity"
$traversalSpec.skip = $false
$traversalSpec.selectSet = new-object SelectionSpec
 
$sspecElement = new-object SelectionSpec
$traversalSpec.selectSet = $sspecElement, $dc2f
$traversalSpec.selectSet[0].name = $traversalSpec.name

$pfSpec = new-object PropertyFilterSpec
$pfSpec.propSet = new-object PropertySpec
$pfSpec.propSet[0].type = "VirtualMachine"
$pfSpec.propSet[0].all = $false
$pfSpec.propSet[0].pathSet = "config.name", "config.uuid", "config.extraConfig", "config.hardware", "guest.net"
#$pfSpec.propSet[0].pathSet = "config.name", "guest.net"

$pfSpec.objectSet = new-object ObjectSpec
$pfSpec.objectSet[0].obj = $rootFolder
$pfSpec.objectSet[0].skip = $false
$pfSpec.objectSet[0].selectSet = new-object SelectionSpec
$pfSpec.objectSet[0].selectSet[0] = $traversalSpec

$pfSpecRes = new-object PropertyFilterSpec 
$pfSpecRes = $pfSpec

$retProp = $service.retrieveProperties($propCollectorRef, $pfSpecRes)

for ($i= 0; $i - $retProp.Length; $i += 1) {
 $ObjContent = $retProp[$i]
 $propSet = $ObjContent.propSet
 for ($y= 0; $y - $propSet.Length; $y += 1) {
  $dynProp = $propSet[$y]
  if (!$dynProp.val.getType().IsArray) {
   write-host $dynProp.Name ": " $dynProp.val
  } else {
   for ($z= 0; $z - $dynProp.Val.Length; $z += 1) {
    $OCdynProp = $dynProp.val[$z]
    If ($OCdynProp.getType().ToString() -eq "GuestNicInfo") {
     write-host "MAC:" $OCdynProp.get_macAddress()
     write-host "IP:" $OCdynProp.get_IPAddress()
     write-host "Network:" $OCdynProp.get_network()
    } else {
     write-host $OCdynProp.get_key() ": " $OCdynProp.get_value()
    }
   }
  }
 }
 write-host
} 


#for ($i= 0; $i - $retProp.Length; $i += 1) {
# $ObjContent = $retProp[$i]
# $propSet = $ObjContent.propSet
# write-host $propSet[0].val "-" $propSet[1].val[0].macAddress "-" $propSet[1].val[0].IPAddress
#
#}
#return
#get-member -i $vmFolderMOR | write-host



Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Monday, June 2, 2008

Spinning Excel Pie Chart

Have you ever wanted to create a pie chart in Excel that spins? I didn't think so, but anyway, below is a little bit of VBA that will do the job if you've got a toggle button on your spreadsheet (ToggleButton1).



Option Explicit

Dim Spinning As Boolean
Dim RotateCalled As Integer

Public Sub Rotate()
'http://msdn.microsoft.com/en-us/library/aa173240(office.11).aspx
    RotateCalled = RotateCalled + 1
    If RotateCalled > 1 Then Exit Sub   ' Only call once
    
    Dim x As Integer
    
    Do
        If Spinning = True Then
            Worksheets("Sheet1").ChartObjects(1).Chart.PieGroups(1).FirstSliceAngle = x
            x = x + 1
            If x = 360 Then x = 0
        End If
        Call Wait(0.05)
    Loop
End Sub

Sub Wait(tSecs As Single)
    Dim sngSec As Single
     
    sngSec = Timer + tSecs
    Do While Timer < sngSec
        DoEvents
    Loop
End Sub

Private Sub ToggleButton1_Click()
    If ToggleButton1.Value = True Then
      Spinning = True
      ToggleButton1.Caption = "Stop"
    Else
      Spinning = False
      ToggleButton1.Caption = "Go"
    End If
    
    Call Rotate
End Sub
 1 Then Exit Sub   ' Only call once
    
    Dim x As Integer
    
    Do
        If Spinning = True Then
            Worksheets("Sheet1").ChartObjects(1).Chart.PieGroups(1).FirstSliceAngle = x
            x = x + 1
            If x = 360 Then x = 0
        End If
        Call Wait(0.05)
    Loop
End Sub

Sub Wait(tSecs As Single)
    Dim sngSec As Single
     
    sngSec = Timer + tSecs
    Do While Timer < sngSec
        DoEvents
    Loop
End Sub

Private Sub ToggleButton1_Click()
    If ToggleButton1.Value = True Then
      Spinning = True
      ToggleButton1.Caption = "Stop"
    Else
      Spinning = False
      ToggleButton1.Caption = "Go"
    End If
    
    Call Rotate
End Sub



Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Friday, May 30, 2008

Poke-Info PowerShell script

I was working next to someone the other day, and there were two monitors connected to each PC, and he had just dragged a script from his right monitor to the left to read. It occurred to me that it would be handy to have a grid-connected view of desktops/monitors, and you could essentially drag and drop windows between computers.

Needless to say this probably isn't going to be achievable in 10 lines of powershell script, but as a version 0.01 of the concept, this script will take a file and a computer as parameters, and copy the file remotely and start notepad on the interactive desktop of the remote machine.



param (
$file = "",
$computer = ""
)


#
# 30/05/2008, Wayne Martin, Initial version
#
# Description:
# Poke a file into a notepad.exe process in the interactive desktop on a remote machine.
#
# Assumptions, this script works on the assumption that:
# psexec.exe is available on the host
# the account running the powershell script has administrative access to the remote computer
# WMI is accessible on the remote machine (queries remote temp directory and windir)
#
# Usage
# Copy a text file to a remote machine and start notepad on the interactive desktop
# . .\PokeInfo.ps1 -f textfile.txt -m RemoteMachine1
#
# Notes
# psexec.exe is required on the host
# This could also be achieved by using win32_Schedulejob interactive now+1, but this assumes schedule is running on the remote machine and time is synchronised
#
# References:
# http://blogs.msdn.com/powershell/archive/2007/01/16/managing-processes-in-powershell.aspx
#

write-output ("Copying and remotely loading " + $file + " on " + $computer)
$envTemp = (get-WMIObject -computerName $computer -class win32_environment -property VariableValue -filter "Name='Temp' AND SystemVariable=True").VariableValue
$windir = (get-WMIObject -computerName $computer -class win32_operatingsystem -property WindowsDirectory).WindowsDirectory

$tempfile = [System.IO.Path]::GetRandomFileName()
$localDest = $envTemp.Replace("%SystemRoot%", $windir) + "\" + $tempfile
$UNCDest = "\\" + $computer + "\" + $localDest.Replace(":", "$")

copy-item -path $file -dest $UNCdest

$cmd = "\\" + $computer + " /i /d notepad " + $localdest
write-output ("Running " + $cmd)
[diagnostics.process]::start("psexec.exe", "$cmd")



Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Friday, May 16, 2008

Modifying Exchange mailbox permissions

Two methods are discussed in this post regarding command-line modifications to Exchange 2003 mailbox security, VBScript and ADModify.Net.

Method 1 - VBScript

The first method is to use a VBScript - modifying exchange mailbox rights can be done in a very similar fashion to modifying the ntSecurityDescriptor attribute. However, to make this work you need CDOEXM installed, installed with the exchange console on your administrative workstation.

The CDOEXM (Collaboration Data Objects for Exchange Management) objects provide an interface called IExchangeMailbox. This interface inherits from IMailboxStore, and also provides an additional property key to modifying Exchange permissions - the MailboxRights property.

Instead of using the msExchMailboxSecurityDescriptor attribute, you need to get and set the DACL using the MailboxRights property. The msExchMailboxSecurityDescriptor is only a back-link from the Exchange store, it doesn’t have a matching forward-link - therefore it won't be replicated if you modify this AD attribute. It eventually would be overwritten when the DACL is replicated back from the exchange mailbox object.

I assume the reason for the backlink msExchMailboxSecurityDescriptor attribute is mostly convenience and backwards compatibility, ie, an application or an admin can read the Exchange permissions (excluding inheritance I think) without requiring CDO.

Note that the Microsoft references below provide example script on using MailboxRights to modify existing mailboxes, and although the articles are quite good and very detailed, the relevance of MailboxRights compared to msExchMailboxSecurityDescriptor wasn't immediately obvious (to me at least).

Method 2 - ADModify.Net

The second method is to use ADModify.Net - a very powerful (and therefore potentially dangerous) utility to perform bulk modifications on mailbox objects. It comes with both command-line and GUI versions.

admodcmd is the command-line version, and the following example command starts at the specified DN, filtering based on user accounts with a certain CN, and then adds full-access mailbox rights to these users for the specified group:

admodcmd -dn "OU=USERS,DC=domain,DC=com" -f "(&(objectClass=user)(objectCategory=person)(CN=user*))" -addtomailboxrights domain\GroupToAdd ACE_MB_FULL_ACCESS


Issuing Query....

12 items found matching the specified filter.


0% 50% 100%
----------------------------------------
...................................................

Successful changes: 12
Already set to specified value: 0
Failed changes: 0

Operation Completed in 4.15625 seconds.


References

How to set Exchange Server 2003 and Exchange 2000 Server mailbox rights on a mailbox that exists in the information store
http://support.microsoft.com/?id=310866

How to set Exchange Server 2000 and 2003 mailbox rights at the time of mailbox creation
http://support.microsoft.com/kb/304935

Exchange 2003 SP2 Disabling Mapi/Non-Cached Access Per User
http://technet.microsoft.com/en-us/library/bb219050(EXCHG.65).aspx

ADModify.NET - Home
http://www.codeplex.com/admodify


Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Thursday, May 15, 2008

PowerShell FindFirstFileW bypassing MAX_PATH

By default it seems PowerShell uses the ANSI versions of FindFirstFile and FindNextFile, and is therefore limited to MAX_PATH - 260 characters in total. This PowerShell script uses in-line compiled VB.Net to call the wide unicode versions - FindFirstFileW and FindNextFileW - to bypass the ANSI MAX_PATH limitations. The results are essentially the same as a 'dir /s/b/a-d' that won't return with 'The filename or extension is too long.' or 'The directory name x is too long' errors.

If you use '/l', the script will only return deep paths, and also provide the 8.3 equivalent to the deep path - a useful method to access these files. For example, using this method, you can access a file with UNC (eg \\server\share) over 18 levels deep ((260-16)/13).

Note that these wide calls only bypass MAX_PATH when using a mapped drive with the \\?\ prefix to disable path parsing. Just specify a mapped drive or local path normally, eg c:\temp, the script will automatically prepend \\?\

I've been experimenting with using PowerShell to dynamically compile VB.Net or C# code, and within that managed code, calling unmanaged platform invoke operations to get to APIs. I like the flexibility of using a scripting language rather than compiled code, and while this certainly isn't as functional as 'dir', it was useful to me when at least trying to get a list of deep files.



## FindFiles.ps1 ##
param(
   [string] $dirRoot = $pwd,
   [string] $Spec = "*.*",
   [bool] $longOnly = $false
   )

# Changes:
#  23/05/2008, Wayne Martin, Added the option to only report +max_path entries, and report the short path of those directories (which makes it easier to access them)
#
#
# Description:
#  Use the wide unicode versions to report a directory listing of all files, including those that exceed the MAX_PATH ANSI limitations
#
# Assumptions, this script works on the assumption that:
#  There's a console to write the output from the compiled VB.Net
#
# Author:
#  Wayne Martin, 15/05/2008
#
# Usage
#  PowerShell . .\FindFiles.ps1 -d c:\temp -s *.*
#
#  PowerShell . .\FindFiles.ps1 -d c:\temp
#
#  PowerShell . .\FindFiles.ps1 -d g: -l $true
#
# References:
#  http://msdn.microsoft.com/en-us/library/aa364418(VS.85).aspx
#  http://blogs.msdn.com/jaredpar/archive/2008/03/14/making-pinvoke-easy.aspx 

$provider = new-object Microsoft.VisualBasic.VBCodeProvider
$params = new-object System.CodeDom.Compiler.CompilerParameters
$params.GenerateInMemory = $True
$refs = "System.dll","Microsoft.VisualBasic.dll"
$params.ReferencedAssemblies.AddRange($refs)

$txtCode = @'
Imports System
Imports System.Runtime.InteropServices
Class FindFiles

Const ERROR_SUCCESS As Long = 0
Private Const MAX_PREFERRED_LENGTH As Long = -1

  _
Public Structure WIN32_FIND_DATAW
    '''DWORD->unsigned int
    Public dwFileAttributes As UInteger
    '''FILETIME->_FILETIME
    Public ftCreationTime As FILETIME
    '''FILETIME->_FILETIME
    Public ftLastAccessTime As FILETIME
    '''FILETIME->_FILETIME
    Public ftLastWriteTime As FILETIME
    '''DWORD->unsigned int
    Public nFileSizeHigh As UInteger
    '''DWORD->unsigned int
    Public nFileSizeLow As UInteger
    '''DWORD->unsigned int
    Public dwReserved0 As UInteger
    '''DWORD->unsigned int
    Public dwReserved1 As UInteger
    '''WCHAR[260]
      _
    Public cFileName As String
    '''WCHAR[14]
      _
    Public cAlternateFileName As String
End Structure

  _
Public Structure FILETIME
    '''DWORD->unsigned int
    Public dwLowDateTime As UInteger
    '''DWORD->unsigned int
    Public dwHighDateTime As UInteger
End Structure

Partial Public Class NativeMethods
   
    '''Return Type: HANDLE->void*
    '''lpFileName: LPCWSTR->WCHAR*
    '''lpFindFileData: LPWIN32_FIND_DATAW->_WIN32_FIND_DATAW*
      _
    Public Shared Function FindFirstFileW( ByVal lpFileName As String,  ByRef lpFindFileData As WIN32_FIND_DATAW) As System.IntPtr
    End Function
  
    '''Return Type: BOOL->int
    '''hFindFile: HANDLE->void*
    '''lpFindFileData: LPWIN32_FIND_DATAW->_WIN32_FIND_DATAW*
      _
    Public Shared Function FindNextFileW( ByVal hFindFile As System.IntPtr,  ByRef lpFindFileData As WIN32_FIND_DATAW) As  Boolean
    End Function

    '''Return Type: BOOL->int
    '''hFindFile: HANDLE->void*
      _
    Public Shared Function FindClose(ByVal hFindFile As System.IntPtr) As  Boolean
    End Function

    '''Return Type: DWORD->unsigned int
    '''lpszLongPath: LPCWSTR->WCHAR*
    '''lpszShortPath: LPWSTR->WCHAR*
    '''cchBuffer: DWORD->unsigned int
      _
    Public Shared Function GetShortPathNameW( ByVal lpszLongPath As String,  ByVal lpszShortPath As System.Text.StringBuilder, ByVal cchBuffer As UInteger) As UInteger
    End Function

End Class


Private Const FILE_ATTRIBUTE_DIRECTORY As Long = &H10
    Dim FFW as New NativeMethods

Function Main(ByVal dirRoot As String, ByVal sFileSpec As String, Byval longOnly As Boolean) As Long
    Dim result As Long

    result = FindFiles(dirRoot, sFileSpec, longOnly)

    main = result          ' Return the result
End Function

Function FindFiles(ByRef sDir As String, ByVal sFileSpec as String, Byval longOnly As Boolean) As Long
    Const MAX_PATH As Integer = 260
    Dim FindFileData as WIN32_FIND_DATAW
    Dim hFile As Long
    Dim sFullPath As String
    Dim sFullFile As String
    Dim length as UInteger
    Dim sShortPath As New System.Text.StringBuilder()


    sFullPath = "\\?\" & sDir

    'console.writeline(sFullPath & "\" & sFileSpec)

    hFile = FFW.FindFirstFileW(sFullPath & "\" & sFileSpec, FindFileData)     ' Find the first object
    if hFile > 0 Then            ' Has something been found?
      If (FindFileData.dwFileAttributes AND FILE_ATTRIBUTE_DIRECTORY)  <> FILE_ATTRIBUTE_DIRECTORY Then  ' Is this a file?
        sFullFile = sFullPath & "\" & FindFileData.cFileName
        If (longOnly AND sFullFile.Length >= MAX_PATH) Then
          length = FFW.GetShortPathNameW(sFullPath, sShortPath, sFullPath.Length) ' GEt the 8.3 path
          console.writeline(sFullFile & " " & sshortpath.ToString())  ' Yes, report the full path and filename
        ElseIf (NOT longOnly)
          console.writeline(sFullFile)
        End If
      End If

      While FFW.FindNextFileW(hFile, FindFileData)        ' For all the items in this directory
        If (FindFileData.dwFileAttributes AND FILE_ATTRIBUTE_DIRECTORY) <> FILE_ATTRIBUTE_DIRECTORY Then ' Is this a file?
          sFullFile = sFullPath & "\" & FindFileData.cFileName
          If (longOnly AND sFullFile.Length >= MAX_PATH) Then
            length = FFW.GetShortPathNameW(sFullPath, sShortPath, sFullPath.Length) ' GEt the 8.3 path
            console.writeline(sFullFile & " " & sshortpath.ToString())  ' Yes, report the full path and filename
          ElseIf (NOT longOnly)
            console.writeline(sFullFile)
          End If
        End If
      End While
      FFW.FindClose(hFile)           ' Close the handle
      FindFileData = Nothing
    End If

    hFile = FFW.FindFirstFileW(sFullPath & "\" & "*.*", FindFileData)      ' Repeat the process looking for sub-directories using *.*
    if hFile > 0 Then
      If (FindFileData.dwFileAttributes AND FILE_ATTRIBUTE_DIRECTORY) AND _
          (FindFileData.cFileName <> ".") AND (FindFileData.cFileName <> "..") Then
        Call FindFiles(sDir & "\" & FindFileData.cFileName, sFileSpec, longOnly)      ' Recurse
      End If

      While FFW.FindNextFileW(hFile, FindFileData)
        If (FindFileData.dwFileAttributes AND FILE_ATTRIBUTE_DIRECTORY) AND _
            (FindFileData.cFileName <> ".") AND (FindFileData.cFileName <> "..") Then
          Call FindFiles(sDir & "\" & FindFileData.cFileName, sFileSpec, longOnly)     ' Recurse
        End If
      End While
      FFW.FindClose(hFile)           ' Close the handle
      FindFileData = Nothing
    End If

End Function

end class

'@


$cr = $provider.CompileAssemblyFromSource($params, $txtCode)
if ($cr.Errors.Count) {
    $codeLines = $txtCode.Split("`n");
    foreach ($ce in $cr.Errors)
    {
        write-host "Error: $($codeLines[$($ce.Line - 1)])"
        write-host $ce
        #$ce out-default
    }
    Throw "INVALID DATA: Errors encountered while compiling code"
 }
$mAssembly = $cr.CompiledAssembly
$instance = $mAssembly.CreateInstance("FindFiles")

$result = $instance.main($dirRoot, $Spec, $longOnly)


Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Thursday, May 8, 2008

Binary <-> Hex String files with Powershell

I needed a method of converting a text representation of a binary file (eg. the text 'AE00F00D' in a file) to the binary byte equivalent and vice versa.

Included are the PowerShell scripts I ended up writing for the task. The core parts of script are below, click 'Read More!' to see the full .PS1 scripts

Note that I have updated the original post, as the '-readcount 0' when reading the file, pre-allocating the array (thanks for the suggestion Phil) and using an assigned variable in the for loop made this much faster.



[byte[]]$bytes = Get-Content -encoding byte -readcount 0 -path $BinaryFile
  for ($i = 0; $i -le $count-1; $i++)
  { $hex = "{0:x}" -f $bytes[$i]
    [void]$output.Append($hex.PadLeft(2, "0"))  # Pad any single digits
  }
    set-content $OutputFile -value $output.ToString()

  $HexString = Get-Content -readcount 0 -path $HexStringFile
  for ( $i = 0; $i -le $count-1; $i+=2 )
  { $bytes[$x] = [byte]::Parse($hexString.Substring($i,2), [System.Globalization.NumberStyles]::HexNumber)
    $x += 1 }
    set-content -encoding byte $OutputFile -value $bytes



I was using this to restore and update some images stored in elements in an Operations Manager 2007 Management Pack XML file.


#HexStringToBinary.ps1

param(
   [string] $HexStringfile = "",
   [string] $OutputFile = ""
   )


if ($HexstringFile -ne "") {
  $HexString = Get-Content -readcount 0 -path $HexStringFile
  $HexString = $HexString[0]
  $count = $hexString.length
  $byteCount = $count/2
  $bytes = New-Object byte[] $byteCount
  $byte = $null

  $x = 0
  for ( $i = 0; $i -le $count-1; $i+=2 )
  { 
    $bytes[$x] = [byte]::Parse($hexString.Substring($i,2), [System.Globalization.NumberStyles]::HexNumber)
    $x += 1
  }

  if ($OutputFile -ne "") {
    set-content -encoding byte $OutputFile -value $bytes
  } else {
    write-host "No output file specified"
  }
} else{
  write-host "Error, no input file specified"
}

# Byte.Parse Method (String, NumberStyles)
# http://msdn.microsoft.com/en-us/library/4eszwye3(VS.80).aspx




#BinaryToHexString.ps1

param(
   [string] $Binaryfile = "",
   [string] $OutputFile = ""
   )

if ($BinaryFile -ne "") {
  [byte[]]$bytes = Get-Content -encoding byte -readcount 0 -path $BinaryFile # Use a readCount of 0 to read all lines at once

  $output = new-object System.Text.StringBuilder # Using stringBuilder seems faster than $a = $a + "a" ?
  $count = $bytes.length    # The loop seems much faster when using a pre-set value?
  for ($i = 0; $i -le $count-1; $i++)
  {
    $hex = "{0:x}" -f $bytes[$i]
    [void]$output.Append($hex.PadLeft(2, "0"))  # Pad any single digits
  }
  if ($OutputFile -ne "") {
    set-content $OutputFile -value $output.ToString()
  } else {
    write-host "No output file specified"
  }
} else{
  write-host "Error, no input file specified"
}

# .net formatting decimal to hex
# http://www.microsoft.com/technet/scriptcenter/topics/winpsh/convert/hex.mspx



Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Monday, April 21, 2008

Unlocking XP/2003 without passwords

The winlogon secure desktop is really just another desktop on winsta0, and I came across a utility - RemoteUnlock.exe - which injects itself as a thread in the console winlogon process of a remote machine and switches from the secure winlogon desktop to the logged on user’s winsta0\default desktop.

This allows you to skip the SAS process requiring the account password of the currently logged on user, and just shows the desktop, allowing full console interaction as the logged on user.

This doesn't really have very many practical uses, but the concept is great and I thought I would share the utility I came across plus some additional thoughts on the topic.

The utility and source code:
http://www.codeproject.com/KB/system/RemoteUnlock.aspx

I've previously toyed with creating a command shell on a remote winlogon desktop, which can be done with the following command:
- psexec /s \\%computer% cmd /c c:\windows\temp\psexec /accepteula /x /d /s cmd

After another discussion regarding secure passwords with PowerShell, I was curious just what might be possible from the Winlogon desktop.

One thing led to another, and after trying unsuccessfully using a PowerShell script run from a command shell on the Winlogon desktop (using psexec) calling GetProcessWindowStation/UnlockWindowStation (an undocumented API, presumably unlocking a window station) and OpenDesktop/SwitchDesktop , I came across a reference to UnlockWindowStation which mentioned that this would only work when running as part of winlogon.exe, explaining why the PowerShell script did nothing.

I then successfully tested remoteunlock.exe running from one XPSP2 workstation against another. RemoteUnlock uses switchdesktop which doesn’t actually unlock the desktop, I was going to recompile and try unlockwindowstation, but I don’t have Visual Studio. It would also be interesting to modify this to work with TS winlogon processes, rather than only the interactive console (I presume you could similarly ‘unlock’ an in-use TS session).

As an aside, below is the PowerShell script calling APIs through VB.Net embedded code, which doesn’t work in this case but it's still a valid example of how to call APIs from PowerShell (which I essentially just copied from http://monadblog.blogspot.com/2005_12_01_archive.html):



$provider = new-object Microsoft.VisualBasic.VBCodeProvider
$params = new-object System.CodeDom.Compiler.CompilerParameters
$params.GenerateInMemory = $True
$refs = "System.dll","Microsoft.VisualBasic.dll"
$params.ReferencedAssemblies.AddRange($refs)


# VB.NET EXAMPLE 
$txtCode = @'
Class FindProcessWinStation
    Declare Auto Function GetProcWinStation Lib “user32.dll” Alias "GetProcessWindowStation" () As Integer
    Declare Auto Function UnlockWinStation Lib “user32.dll” Alias "UnlockWindowStation" (ByVal WinSta As Integer) As Integer
    Declare Auto Function OpenWinStation Lib “user32.dll” Alias "OpenWindowStation" (ByVal lpszWinSta As String, ByVal fInherit as Boolean, ByVal ACCESS_MASK as Integer) As Integer
    Declare Auto Function OpenDesktop Lib “user32.dll” Alias "OpenDesktop" (ByVal lpszDesktop As String, ByVal dwFlags as Integer, ByVal fInherit as Boolean, ByVal ACCESS_MASK as Integer) As Integer
    Declare Auto Function SwitchDesktop Lib “user32.dll” Alias "SwitchDesktop" (ByVal hDesktop As Integer) As Integer
    Function Main()
        main = GetProcWinStation()
'        UnlockWinStation(main)
'        main = OpenWinStation("winsta0\\desktop", True, 895)
        main = OpenDesktop("Default", 0, True, 256)
        SwitchDesktop(main)

    End Function
end class
'@



$results = $provider.CompileAssemblyFromSource($params, $txtCode)
$mAssembly = $results.CompiledAssembly
$i = $mAssembly.CreateInstance("FindProcessWinStation")
$r = $i.main()

write-host $r



Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Wednesday, April 16, 2008

Purging aged files from the filesystem

This post provides information on a simple batch file to purge files based on a specified age. There are many more clever ways to achieve this - VBScript, PowerShell, cleanmgr job, but this is a very simple method that provides all the functionality with verbose logging, with next to no effort.

To use this, copy the text below to a batch file, and then schedule it to run every day/week. There are only two lines required - using Robocopy to move the files, and then another call to delete the files. It would be even easier if Robocopy supported a NUL destination, but there are advantages to moving and then deleting, for example you could move files every day, and then delete them weekly/monthly.

Note that the target below is the %temp% directory, usually on C: drive. Therefore if you are purging large quantities of data from a disk other than C:, using the system/boot disk is not really appropriate, perhaps change to a directory on the disk you are purging (which would therefore not require any extra space as the files are being moved locally).

--

Set FileAge=30

Set Directory=c:\Dir\To\Purge
Set PurgeDir=%Temp%\Purge_%Random%

for /f "tokens=1-8 delims=/:. " %%i in ('echo %date%') do Set DateFlat=%%l%%k%%j
Set LogFile=c:\logs\%~n0_%DateFlat%.log

Echo %Date% %Time%: Purging files from %Directory% older than %FileAge% days, logfile: %LogFile% >> %LogFile%
robocopy %Directory% "%PurgeDir%" *.* /minage:%FileAge% /v /fp /ts /mov /e /r:1 /w:1 /log+:%LogFile%

If Exist "%PurgeDir%" echo Deleting files moved with robocopy: 'rd /s /q "%PurgeDir%"' >> %LogFile%
If Exist "%PurgeDir%" rd /s /q "%PurgeDir%"


--


References:
how to create your own registration of an existing handler (such as the disk cleanup handler).
http://msdn2.microsoft.com/en-us/library/bb776782(VS.85).aspx


Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

Tuesday, April 1, 2008

Audit Windows 2003 print server usage

This post provides information on a solution to provide audit logs and summary information on printer usage on a Windows print server. This will provide daily and monthly printer event logs, and summary results based on one or more log files.

Pre-requisites:

  • A scheduled task that runs every day to collect and process the logs, it doesn’t have to be on the print server.
  • 'Log Spooler Information Events' on the printer spooler in question, which will write Event ID 10 entries every time someone prints through the spooler
  • A system event log big enough to capture at least one day's logs

Create a batch file that contains the following commands. Note that you will need to modify the variables or to reference paths as appropriate, eg for dumpel.exe and the VBScript, and the print/log dir.

Set PrintDir=c:\Print
Set LogDir=C:\logs
for /f "tokens=1-8 delims=/:. " %%i in ('echo %date%') do Set DateFlat=%%l%%k%%j
dumpel -s \\%print_server% -l System -e 10 -m Print -d 1 >> %logDir%\%server%_jobs_%DateFlat%.csv
for /f "tokens=3,4 delims=/ " %%i in ('echo %date%') do copy %server%_jobs_%%j%%i??.csv %PrintDir%\PrintJobs_%%j%%i.csv /y
cscript ProcessPrinterLogs.wsf /f:%LogDir%


If you create a scheduled task to run this batch file every day at the same time, these commands will:

  1. Dump the event logs for the past day to a daily log file with YYYYMMDD suffix.
  2. Collate the daily log files into a monthly log file, by appending each daily file. For each day in a month, this command will overwrite the previous monthly log, until it runs on the last day of the month.
  3. The script processes the dumpel log entries, providing different views in the form of per-printer, per-user, per-day totals of jobs/pages/bytes (useful for graphics), as well as summary totals of the information.

This has the following advantages:

  • It’s simple and not very intensive. A SQL database with a recurring DTS job to import the logs and then using SQL Reporting Services would be a lot prettier, but really not that much more functional or useful.
  • You will have a permanent set of log files, one for each month that you can store for historical purposes, while purging the daily log file directory every so often.

The ProcessPrinterLogs.vbs script that does all the work is listed below. To run:
cscript ProcessPrinterLogs.vbs /f:%logDir%



Const ForReading = 1, ForWriting = 2, ForAppending = 8

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("WScript.Shell")

Main()


Sub Main()
    If WScript.Arguments.Named.Exists("f") Then
        sSource = Wscript.Arguments.Named("f")
    Else
        Wscript.Arguments.ShowUsage()
        Wscript.Echo "Source file or directory must be supplied"
        Wscript.Quit(2)
    End If

    If Wscript.Arguments.Named.Exists("o") Then
        sOutputFile = Wscript.Arguments.Named("o")
    Else
        dNow = Now    
        dLogDate = DatePart("yyyy", dNow) 
        dLogDate = dLogDate & String(2 - Len(DatePart("m", dNow)),"0") & DatePart("m", dNow)
        dLogDate = dLogDate & String(2 - Len(DatePart("d", dNow)),"0") & DatePart("d", dNow)    
        sOutputFile = objShell.ExpandEnvironmentStrings("%Temp%")
        sOutputFile = sOutputFile & "\" & Left(WScript.ScriptName, InStrRev(WScript.ScriptName,".vbs")-1) & "_" & dLogDate & ".csv"
    End If

    wscript.echo "Input file/dir: '" & sSource & "'"
    wscript.echo "Output file: '" & sOutputFile & "'"


    If objFSO.FileExists(sSource) Then 
        sFileSet = sSource                                        ' Process a single file
        wscript.echo "Single file specified - " & sFileSet
    ElseIf objFSO.FolderExists(sSource) Then
        wscript.echo "Source specified was a directory, reading files from '" & sSource & "'"
        sFileSet = ""
        Set oFolder = objFSO.GetFolder(sSource)                                ' Get the folder
        Set oFiles = oFolder.Files
        For Each oFile in oFiles                                    ' For each file
            sFileset = sFileset & vbCRLF & oFile.Path                         ' Append to the fileset
        Next
        If Len(sFileSet) > Len(vbCRLF) Then sFileSet = Right(sFileSet, Len(sFileSet) - Len(vbCRLF))    ' Trim the leading CRLF
    End If

    Set dPrinters  = CreateObject("Scripting.Dictionary")                            ' Create the dictionary objects
    Set dusers = CreateObject("Scripting.Dictionary")
    Set dDates = CreateObject("Scripting.Dictionary")
    Set dJobs = CreateObject("Scripting.Dictionary")

    For Each sFile in Split(sFileset, vbCRLF)                                ' For Each file
        wscript.echo "Processing '" & sFile & "'"
        sBuffer = ""
           Set objTextStream = objFSO.OpenTextFile(sFile, ForReading)      
        sBuffer = objTextStream.ReadAll

        For Each sLine in Split(sBuffer, vbCRLF)                            ' For each line in this file
            Call ProcessLogEntry(sLine, dPrinters, dUsers, dDates, dJobs)                ' Process the log entry
        Next
    Next

    Call ProduceOutput(sOutput, dPrinters, dUsers, dDates, dJobs)                        ' Produce the output
    Set objTextStream = objFSO.OpenTextFile(sOutputFile, ForWriting, True)
    objTextStream.Write sOutput
    wscript.echo "Output saved to '" & sOutputFile & "', " & Len(sOutput) & " characters."

End Sub

Function ProduceOutput(ByRef sOutput, ByRef dPrinters, ByRef dUsers, ByRef dDates, ByRef dJobs)
    Dim strPrinter, strPort, dtmDate, strUser, strserver, strDocumentName, intSize, intPages, strInformation, strTotal
    Dim strUserTotal, strPrinterTotal, strDateTotal, strJobTotal, aJobTotal

    sOutput = ""
    For Each strPrinter in dPrinters.Keys        
        sOutput = sOutput & vbCRLF & strPrinter & "," & dPrinters.Item(strPrinter)
    Next

    sOutput = sOutput & vbCRLF
    For Each strUser in dUsers.Keys
        sOutput = sOutput & vbCRLF & strUser & "," & dUsers.Item(strUser)
    Next

    sOutput = sOutput & vbCRLF
    For Each dtmDate in dDates.Keys
        sOutput = sOutput & vbCRLF & dtmDate & "," & dDates.Item(dtmDate)
    Next

    sOutput = sOutput & vbCRLF
    For Each strTotal in dJobs.Keys
        strJobTotal = dJobs.Item(strTotal)
        aJobTotal = Split(strJobTotal, ",")
        sOutput = sOutput & vbCRLF & "Total Jobs," & aJobTotal(0)
        sOutput = sOutput & vbCRLF & "Total Pages," & aJobTotal(1)
        sOutput = sOutput & vbCRLF & "Total Size (MB)," & aJobTotal(2)
    Next

    sOutput = sOutput & vbCRLF
    strUserTotal = UBound(dUsers.Keys)+1
    strPrinterTotal = UBound(dPrinters.Keys)+1
    strDateTotal = UBound(dDates.Keys)+1
    sOutput = sOutput & vbCRLF & "Printers," & strPrinterTotal 
    sOutput = sOutput & vbCRLF & "Users," & strUserTotal 
    sOutput = sOutput & vbCRLF & "Days," & strDateTotal 

    aJobTotal = Split(strJobTotal, ",")
    sOutput = sOutput & vbCRLF

    sOutput = sOutput & vbCRLF & "Average jobs/person," & CInt(aJobTotal(0)/strUserTotal)
    sOutput = sOutput & vbCRLF & "Average pages/person," & CInt(aJobTotal(1)/strUserTotal)
    sOutput = sOutput & vbCRLF & "Average pages/person/day," & CInt(CInt(aJobTotal(1)/strUserTotal) / strDateTotal)
    sOutput = sOutput & vbCRLF & "Average pages/minute," & CInt(aJobTotal(1) / (strDateTotal * 8 * 60))

End Function

Function ProcessLogEntry(ByRef sLine, ByRef dPrinters, ByRef dUsers, ByRef dDates, ByRef dJobs)
    Dim strPrinter, strPort, dtmDate, strUser, strserver, strDocumentName, intSize, intPages, strInformation 
    Dim aPrintJob, intOffset, strTemp, aTemp

    aPrintJob = Split(sLine, vbTAB)


    If UBound(aPrintJob) = 9 Then
        dtmDate = aPrintJob(0) ' & " " & aPrintJob(1)
        aTemp = Split(dtmDate, "/")
        dtmDate = Right("00" & Trim(aTemp(1)), 2) & "/" & Right("00" & Trim(aTemp(0)), 2) & "/" & aTemp(2)        ' Trim, pad and switch to dd/mm/yyyy instead of mm/dd/yyyy
        strServer = aPrintJob(8)

        strInformation = Trim(aPrintJob(9))
        strInformation = Right(strInformation, Len(strInformation) - InStr(strInformation, " "))    ' Remove the job ID
        intOffset = InStrRev(strInformation, " ")
        intPages = Right(strInformation, Len(strInformation) - intOffset)        ' Extract the number of pages from the end
        strInformation = Left(strInformation, intOffset-1)                ' Trim the string
    
        intOffset = InStrRev(strInformation, " ")
        intSize = Right(strInformation, Len(strInformation) - intOffset)        ' Extract the number of bytes from the end
        strInformation = Left(strInformation, intOffset-1)                ' Trim the string    
    
        intOffset = InStrRev(strInformation, " ")
        strPort = Right(strInformation, Len(strInformation) - intOffset)        ' Extract the port from the end
        strInformation = Left(strInformation, intOffset-1)                ' Trim the string    
    
        intOffset = InStrRev(strInformation, " ")
        strPrinter = Right(strInformation, Len(strInformation) - intOffset)        ' Extract the printer from the end
        strInformation = Left(strInformation, intOffset-1)                ' Trim the string    
    
        intOffset = InStrRev(strInformation, " ")
        strUser = Right(strInformation, Len(strInformation) - intOffset)        ' Extract the user from the end
        strInformation = Left(strInformation, intOffset-1)                ' Trim the string    
    
        strDocumentName = strInformation

        If dPrinters.Exists(strPrinter) Then                         ' Does this printer already exist in the dictionary?
            aTemp = Split(dPrinters.Item(strPrinter), ",")                ' Find the existing printer job/page count
            aTemp(0) = aTemp(0) + 1                            ' Increment the job count
            aTemp(1) = aTemp(1) + CInt(intPages)                    ' Add to the page count
            aTemp(2) = aTemp(2) + CInt(intSize/1024/1024)                ' Add to the byte count
            dPrinters.Item(strPrinter) = Join(aTemp, ",")                ' Update the dictionary
        Else
            aTemp = Array(1, intPages, CInt(intsize /1024/1024))            ' Start the job/page count
            dPrinters.Add strPrinter, Join(aTemp, ",")                ' Create this item
        End If
    
        If dUsers.Exists(strUser) Then                             ' Does this user already exist in the dictionary?
            aTemp = Split(dUsers.Item(strUser), ",")                ' Find the existing user job/page count
            aTemp(0) = aTemp(0) + 1                            ' Increment the job count
            aTemp(1) = aTemp(1) + CInt(intPages)                    ' Add to the page count
            aTemp(2) = aTemp(2) + CInt(intSize/1024/1024)                ' Add to the byte count
            dUsers.Item(strUser) = Join(aTemp, ",")                    ' Update the dictionary
        Else
            aTemp = Array(1, intPages, CInt(intsize /1024/1024))            ' Start the job/page count
            dUsers.Add strUser, Join(aTemp, ",")                    ' Create this item
        End If

        If dDates.Exists(dtmDate) Then                             ' Does this date already exist in the dictionary?
            aTemp = Split(dDates.Item(dtmDate), ",")                ' Find the existing date job/page count
            aTemp(0) = aTemp(0) + 1                            ' Increment the job count
            aTemp(1) = aTemp(1) + CInt(intPages)                    ' Add to the page count
            aTemp(2) = aTemp(2) + CInt(intSize/1024/1024)                ' Add to the byte count
            dDates.Item(dtmDate) = Join(aTemp, ",")                    ' Update the dictionary
        Else
            aTemp = Array(1, intPages, CInt(intsize /1024/1024))            ' Start the job/page count
            dDates.Add dtmDate, Join(aTemp, ",")                    ' Create this item
        End If

        If dJobs.Exists(JOB_TOTAL) Then                         ' Does the total already exist in the dictionary?
            aTemp = Split(dJobs.Item(JOB_TOTAL), ",")                ' Find the existing total counts
            aTemp(0) = aTemp(0) + 1                            ' Increment the job count
            aTemp(1) = aTemp(1) + CInt(intPages)                    ' Add to the page count
            aTemp(2) = aTemp(2) + CInt(intSize/1024/1024)                ' Add to the byte count
            dJobs.Item(JOB_TOTAL) = Join(aTemp, ",")                ' Update the dictionary
        Else
            aTemp = Array(1, intPages, CInt(intsize /1024/1024))            ' Start the job/page count
            dJobs.Add JOB_TOTAL, Join(aTemp, ",")                    ' Create this item
        End If
    Else
        wscript.echo "skipped '" & sLine & "'"
    End If
End Function


* Please don’t print this post :) *

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.


Read more!

All Posts

printQueue AD objects for 2003 ClusterVirtualCenter Physical to VirtualVirtual 2003 MSCS Cluster in ESX VI3
Finding duplicate DNS recordsCommand-line automation – Echo and macrosCommand-line automation – set
Command-line automation - errorlevels and ifCommand-line automation - find and findstrBuilding blocks of command-line automation - FOR
Useful PowerShell command-line operationsMSCS 2003 Cluster Virtual Server ComponentsServer-side process for simple file access
OpsMgr 2007 performance script - VMware datastores...Enumerating URLs in Internet ExplorerNTLM Trusts between 2003 and NT4
2003 Servers with Hibernation enabledReading Shortcuts with PowerShell and VBSModifying DLL Resources
Automatically mapping printersSimple string encryption with PowerShellUseful NTFS and security command-line operations
Useful Windows Printer command-line operationsUseful Windows MSCS Cluster command-line operation...Useful VMware ESX and VC command-line operations
Useful general command-line operationsUseful DNS, DHCP and WINS command-line operationsUseful Active Directory command-line operations
Useful command-linesCreating secedit templates with PowerShellFixing Permissions with NTFS intra-volume moves
Converting filetime with vbs and PowerShellDifference between bat and cmdReplica Domain for Authentication
Troubleshooting Windows PrintingRenaming a user account in ADOpsMgr 2007 Reports - Sorting, Filtering, Charting...
WMIC XSL CSV output formattingEnumerating File Server ResourcesWMIC Custom Alias and Format
AD site discoveryPassing Parameters between OpsMgr and SSRSAnalyzing Windows Kernel Dumps
Process list with command-line argumentsOpsMgr 2007 Customized Reporting - SQL QueriesPreventing accidental NTFS data moves
FSRM and NTFS Quotas in 2003 R2PowerShell Deleting NTFS Alternate Data StreamsNTFS links - reparse, symbolic, hard, junction
IE Warnings when files are executedPowerShell Low-level keyboard hookCross-forest authentication and GP processing
Deleting Invalid SMS 2003 Distribution PointsCross-forest authentication and site synchronizati...Determining AD attribute replication
AD Security vs Distribution GroupsTroubleshooting cross-forest trust secure channels...RIS cross-domain access
Large SMS Web Reports return Error 500Troubleshooting SMS 2003 MP and SLPRemotely determine physical memory
VMware SDK with PowershellSpinning Excel Pie ChartPoke-Info PowerShell script
Reading web content with PowerShellAutomated Cluster File Security and PurgingManaging printers at the command-line
File System Filters and minifiltersOpsMgr 2007 SSRS Reports using SQL 2005 XMLAccess Based Enumeration in 2003 and MSCS
Find VM snapshots in ESX/VCComparing MSCS/VMware/DFS File & PrintModifying Exchange mailbox permissions
Nested 'for /f' catch-allPowerShell FindFirstFileW bypassing MAX_PATHRunning PowerSell Scripts from ASP.Net
Binary <-> Hex String files with PowershellOpsMgr 2007 Current Performance InstancesImpersonating a user without passwords
Running a process in the secure winlogon desktopShadow an XP Terminal Services sessionFind where a user is logged on from
Active Directory _msdcs DNS zonesUnlocking XP/2003 without passwords2003 Cluster-enabled scheduled tasks
Purging aged files from the filesystemFinding customised ADM templates in ADDomain local security groups for cross-forest secu...
Account Management eventlog auditingVMware cluster/Virtual Center StatisticsRunning scheduled tasks as a non-administrator
Audit Windows 2003 print server usageActive Directory DiagnosticsViewing NTFS information with nfi and diskedit
Performance Tuning for 2003 File ServersChecking ESX/VC VMs for snapshotsShowing non-persistent devices in device manager
Implementing an MSCS 2003 server clusterFinding users on a subnetWMI filter for subnet filtered Group Policy
Testing DNS records for scavengingRefreshing Computer Account AD Group MembershipTesting Network Ports from Windows
Using Recovery Console with RISPAE Boot.ini Switch for DEP or 4GB+ memoryUsing 32-bit COM objects on x64 platforms
Active Directory Organizational Unit (OU) DesignTroubleshooting computer accounts in an Active Dir...260+ character MAX_PATH limitations in filenames
Create or modify a security template for NTFS perm...Find where a user is connecting from through WMISDDL syntax in secedit security templates

About Me

I’ve worked in IT for over 20 years, and I know just about enough to realise that I don’t know very much.