Labels

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.

6 comments:

Anonymous said...

Is there any way I can get the client name from which a print job originated?

regards
jan

Brian E. Tower said...

Hey Wayne, great post! I used your scripts as a starting point for my print server auditing needs.

In addition to modifying the batch portion to loop through a list of servers, I've also added a section to the vbs script that outputs a list of the users printing to each printer. I also used this opportunity to learn VBS as I've not worked with it before.

I'm willing to share with you my additions and would like to know how you're using the output to generate reports, charts, etc. Please feel free to contact me at towerbe@gmail.com if you're interested.

bt

Brian E. Tower said...
This comment has been removed by the author.
Brian E. Tower said...

Hey Wayne,

I've gotten a number of emails from people asking me about my version of your script(s). Hope you don't mind if I use this post to direct them to my blog on the subject.

http://pyreanspring.blogspot.com/2008/09/auditing-windows-2003-print-servers.html

On a related note, I also have a post on a script I developed to unmap and remap printers, changing the print server used.

http://pyreanspring.blogspot.com/2008/10/more-fun-with-windows-printers.html

bt

Unknown said...

Hi Wayne,

cool script.

It so happens that I just recently did a powershell version of a print monitor for the same scenario.

It has been uploaded to MS Script repository, check it out

http://gallery.technet.microsoft.com/ScriptCenter/en-us/44ba12ff-8c7f-41ad-8d7f-85421ba4198b

Regards Konráð Hall

mayavi said...

Hi

I am looking for a script to work with windows 2008 print server!


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.