Small code with powerful results, the occasional opinion … and beer. 

14 Feb 2008
VBScript code to beautify VBScript code

Sounds redundant, but it’s a utility function you can use when NotePad, TextPad, Context or another text editor (e.g. textarea of a browser) is used for developing the script. I’ve used this function in a web page which allows building and testing small utility functions. In that page, once the code is submitted and executed, it returns the results and the beautified code. The script writer can stay focused on functionality–not format.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
Function BeautifyVBS (sSource, nTabSpacing)
' Takes VBScript source code and rebuilds the indentation.
Dim sRawLine, sLine, sTest, iIndentIndex, iIndex, oS, sWhiteSpace, bAdjustIndent, bInQuote, aRows
 
Dim aKey(34)
 
Const INDENT_String = 0
Const INDENT_Exeception_String = 1
Const INDENT_Pre_Indent = 2
Const INDENT_Post_Indent = 3
 
' The indent and unindent list is as complete as I could make it (from the MS VBScript reference).
aKey (0) = ARRAY ("if ", " then", 0, 1)
aKey (1) = ARRAY ("select ", "", 0, 2)
aKey (2) = ARRAY ("sub ", "", 0, 1)
aKey (3) = ARRAY ("function ", "", 0, 1)
aKey (4) = ARRAY ("do ","", 0, 1)
aKey (5) = ARRAY ("while ","", 0, 1)
aKey (6) = ARRAY ("for ","", 0, 1)
aKey (7) = ARRAY ("case ", "", -1, 1)
aKey (8) = ARRAY ("with ","", 0, 1)
aKey (9) = ARRAY ("class ","", 0, 1)
aKey (10) = ARRAY ("public sub","", 0, 1)
aKey (11) = ARRAY ("private sub ","", 0, 1)
aKey (12) = ARRAY ("public function ","", 0, 1)
aKey (13) = ARRAY ("private function ","", 0, 1)
aKey (14) = ARRAY ("property get ","", 0, 1)
aKey (15) = ARRAY ("public property get ","", 0, 1)
aKey (16) = ARRAY ("private property get ","", 0, 1)
aKey (17) = ARRAY ("property let ","", 0, 1)
aKey (18) = ARRAY ("public property let ","", 0, 1)
aKey (19) = ARRAY ("private property let ","", 0, 1)
aKey (20) = ARRAY ("property set ","", 0, 1)
aKey (21) = ARRAY ("public property set ","", 0, 1)
aKey (22) = ARRAY ("private property set ","", 0, 1)
aKey (23) = ARRAY ("else ","", -1, 1)
aKey (24) = ARRAY ("elseif ","", -1, 1)
aKey (25) = ARRAY ("end if", "", -1, 0)
aKey (26) = ARRAY ("end select", "", -2, 0)
aKey (27) = ARRAY ("end sub", "", -1, 0)
aKey (28) = ARRAY ("end function", "", -1, 0)
aKey (29) = ARRAY ("loop", "", -1, 0)
aKey (30) = ARRAY ("wend", "", -1, 0)
aKey (31) = ARRAY ("next", "", -1, 0)
aKey (32) = ARRAY ("end class", "", -1, 0)
aKey (33) = ARRAY ("end property", "", -1, 0)
aKey (34) = ARRAY ("end with", "", -1, 0)
 
sWhiteSpace = " " & vbTab
 
Set oS = CreateObject("ADODB.Stream")
oS.Type = 2   ' ASCII
oS.Open
 
iIndentIndex = 0
For Each sRawLine in Split (sSource, vbCrLf)
 
' Remove all whitespace on the left
iIndex = 1
If Len (sRawLine) > 0 Then
Do While iIndex <= Len (sRawLine)
If Instr (sWhiteSpace, Mid (sRawLine, iIndex, 1)) = 0 Then
Exit Do
End If
iIndex = iIndex + 1
Loop
End If
If iIndex > Len (sRawLine) Then
sLine = ""
Else
sLine = Mid (sRawLine, iIndex)
End If
 
' Remove all whitespace on the right
iIndex = Len (sLine)
Do While iIndex > 0
If Instr (sWhiteSpace, Mid (sLine, iIndex, 1)) = 0 Then
Exit Do
End If
iIndex = iIndex - 1
Loop
If iIndex < Len (sLine) Then
sLine = Left (sLine, iIndex)
End If
 
sTest = LCase (LTrim (sLine))
' Find any in-line comment marker, and truncate the comment if it exists.
bInQuote = False
For iIndex = 1 To Len (sTest)
If Not bInQuote And Mid (sTest, iIndex, 1) = "'" Then
Exit For
End If
If Mid (sTest, iIndex, 1) = """" Then
bInQuote = Not bInQuote
End If
Next
If iIndex < Len (sTest) Then  ' Truncate comment
sTest = Left (sTest, iIndex - 1)
' Truncate whitespace again
iIndex = Len (sTest)
Do While iIndex > 0
If Instr (sWhiteSpace, Mid (sTest, iIndex, 1)) = 0 Then
Exit Do
End If
iIndex = iIndex - 1
Loop
If iIndex < Len (sTest) Then
sTest = Left (sTest, iIndex)
End If
End If
 
sTest = LCase (LTrim (sTest)) & SPACE (32)
 
' Adjust Indentation as needed
bAdjustIndent = False
For iIndex = 0 To UBound (aKey, 1)
If Left (sTest, LEN (aKey(iIndex)(INDENT_String))) = aKey(iIndex)(INDENT_String) Then
If LEN(aKey(iIndex)(INDENT_Exeception_String)) = 0 Or Right (RTrim (sTest), LEN (aKey(iIndex)(INDENT_Exeception_String))) = aKey(iIndex)(INDENT_Exeception_String) Then
bAdjustIndent = True
Exit For
End If
End If
Next
 
If bAdjustIndent Then
iIndentIndex = iIndentIndex + aKey(iIndex)(INDENT_Pre_Indent)
If iIndentIndex < 0 Then iIndentIndex = 0
End If
 
If nTabSpacing <= 0 Then
oS.WriteText STRING (iIndentIndex, vbTab) & sLine & vbCrLf
Else
oS.WriteText SPACE (nTabSpacing * iIndentIndex) & sLine & vbCrLf
End If
 
If bAdjustIndent Then
iIndentIndex = iIndentIndex + aKey(iIndex)(INDENT_Post_Indent)
If iIndentIndex < 0 Then iIndentIndex = 0
End If
Next
 
oS.Position = 0
BeautifyVBS = oS.ReadText (-1)
oS.Close()
Set oS = Nothing
End Function

14 Feb 2008
“Touching” a file’s last-modified time stamp

The Unix world has long had utilities like “touch”, and DOS/Windows console applications exist for this: to bump the file’s last modified date/time to the current system time. In Windows 95/98, I used shdate which added a tab on a file or folder’s property which allowed changing the time stamp. Unfortunately, I haven’t seen it work well with XP.

So assuming that all you want to do is change the last-modified date to the current time, for one or more files, the following script will do it for you. Just save it to a location and file name of your choice, then execute the script. That will inject a context-menu item into the HKEY_CLASSES_ROOT\* called “touch”. Just select one or more files in Windows Explorer, and select “Touch” from the context menu.

That’s it. Enjoy. ( touch.vbs )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
' Touch.vbs -- Take 1 or more files, and changes the last modified date to the current time.
' John J Schultz -- Public Domain.
'
' Parameters
'   wscript //NOLOGO //B TouchFiles.vbs {file1} [{file3} [...]]
'
' ----------------------------
 
Option Explicit
 
Dim aFiles, oShell, oFS, oFolder, oArgs, iCount, bValidParameters, sLine, dTimestamp
 
set oShell = WScript.CreateObject("WScript.Shell")
sLine = ""
on error resume next
sLine = oShell.RegRead ("HKCR\*\shell\Touch\")
on error goto 0
If sLine = "" Then
	oShell.RegWrite "HKCR\*\shell\Touch\", "Touch", "REG_SZ"
	oShell.RegWrite "HKCR\*\shell\Touch\command\", "wscript.exe """ & WScript.ScriptFullName & """ ""%1""", "REG_SZ"
	MsgBox "You can now touch any file from the context menu in Windows Explorer, by right-clicking on the file and selecting Touch", vbOKonly + vbInformation, "Shell extension installed."
	Set oShell = Nothing
	WScript.Quit (0)
End If
Set oShell = Nothing
 
Set oArgs = WScript.Arguments
bValidParameters = (oArgs.Count >= 1)
If bValidParameters Then
	For iCount = 0 to oArgs.Count - 1
		If iCount = 0 Then
			ReDim aFiles (iCount)
		Else
			ReDim Preserve aFiles (iCount)
		End If
		aFiles (iCount) = oArgs(iCount)
	Next
End If
Set oArgs = Nothing
 
If Not bValidParameters Then
	WScript.Echo "Invalid parameters"
	WScript.Quit(0)
End If
 
dTimeStamp = Now()
Set oFS = CreateObject("Scripting.FileSystemObject")
Set oShell = CreateObject("Shell.Application")
For iCount = 0 To UBound (aFiles, 1)
	If Not oFS.FileExists (aFiles (iCount)) Then
		WScript.Echo "Skipping missing file: " & aFiles (iCount)
	Else
		Set oFolder = oShell.NameSpace(oFS.GetParentFolderName (aFiles (iCount)))
		oFolder.Items.Item(oFS.GetFileName (aFiles (iCount))).ModifyDate = dTimeStamp
		Set oFolder = Nothing
	End If
Next ' File
Set oShell = Nothing
Set oFS = Nothing

06 Sep 2007
Deleting Old Files

I’ve had this script around for some time, and thought I would share it. There are many utilities that will do this, but this one is designed for VBScript, and uses regular expressions for the file pattern. This allows for almost surgical removal of files from a working folder or archive folder. I’ve used this script for some time on several servers I work with, and it’s pretty reliable.

You can also execute the script in the console (with CScript), and it will display a list of available parameters. Enjoy.

You can download the code here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
' DeleteAgedFiles.vbs -- Remove files by any combination of timestamp aging, file pattern.
' John J Schultz -- 12/12/2005 -- Public Domain.
'
' //B is the switch recommended for production use with wscript to suppress operator messages from
' the script engine (e.g. when launched from a timer).  Due to the number of WScript.Echo statements used,
' Cscript is highly recommended--even when testing.
 
' ----------------------------
' Parameters
Dim bAggressive
Dim bTestRun
Dim bIncludeSubFolders
Dim bSpareRootFolder
Dim bRemoveEmptyFolders
Dim bBatchMode
Dim bUserConfirmationForDelete
Dim bIgnoreZeroLengthFiles
Dim nDateToCheck
Dim nDays
Dim sStartFolder
Dim sFilePattern
Dim nPreserveFileCount
 
' ----------------------------
' Global
Dim dCutOff
Dim iErr, sErr
 
' ###################################################################################
' Support Functions and Subs
Sub LogAnEvent (iType, sMessage)
	Set WshShell = WScript.CreateObject("WScript.Shell")
	WshShell.LogEvent iType, sMessage
	Set WshShell = Nothing
End Sub
 
Function DateToAnsi (dTime)
   DateToAnsi = _
   Year(dTime) & Right ("0" & Month (dTime), 2) & Right ("0" & Day (dTime), 2) & _
   Hour(dTime) & Right ("0" & Minute (dTime), 2) & Right ("0" & Second (dTime), 2)
End Function
 
Sub QuickSort(ByRef aKey, nLowBound, nHighBound)
	' Single dimension array only.
	Dim vPivot, nLowSwap, nHighSwap, vTemp
 
	If IsEmpty (nLowBound) Then
		nLowBound = LBound (aKey)
	End If
	If IsEmpty (nHighBound) Then
		nHighBound = UBound (aKey)
	End If
 
	' Two items to sort
	If nHighBound - nLowBound = 1 Then
		If aKey(nLowBound) > aKey(nHighBound) Then
			vTemp = aKey(nLowBound)
			aKey(nLowBound) = aKey(nHighBound)
			aKey(nHighBound) = vTemp
		End If
	End If
 
	' Three or more items to sort
 
	vPivot = aKey(int((nLowBound + nHighBound) / 2))
	aKey(int((nLowBound + nHighBound) / 2)) = aKey(nLowBound)
	aKey(nLowBound) = vPivot
 
	nLowSwap = nLowBound + 1
	nHighSwap = nHighBound
 
	Do
		' Find the right nLowSwap
		While nLowSwap < nHighSwap and aKey(nLowSwap) <= vPivot
			nLowSwap = nLowSwap + 1
		Wend
		' Find the right nHighSwap
		While aKey(nHighSwap) > vPivot
			nHighSwap = nHighSwap - 1
		Wend
		' Swap values if out of sort order
		If nLowSwap < nHighSwap Then
			vTemp = aKey(nLowSwap)
			aKey(nLowSwap) = aKey(nHighSwap)
			aKey(nHighSwap) = vTemp
		End If
	Loop While nLowSwap < nHighSwap
 
	aKey(nLowBound) = aKey(nHighSwap)
	aKey(nHighSwap) = vPivot
 
	' Recursive call
	' 2 or more items in first section
	If nLowBound < (nHighSwap - 1) Then Call QuickSort(aKey, nLowBound, nHighSwap - 1)
	' 2 or more items in second section
	If nHighSwap + 1 < nHighBound Then Call QuickSort(aKey, nHighSwap + 1, nHighBound)
 
End Sub
 
Function FolderExists (sFolderName)
	Dim oFS
 
	Set oFS = CreateObject("Scripting.FileSystemObject")
	FolderExists = oFS.FolderExists(sFolderName)
	set oFS = Nothing
End Function
 
Function FileMatchesPattern (sFile, sPattern, bIgnoreCase)
    ' Returns true if the file name matches the regular expression in sPattern.
    Dim oRegEx
 
    Set oRegEx = New RegExp
    With oRegEx
	    .Pattern = sPattern
	    .IgnoreCase = bIgnoreCase
	    FileMatchesPattern = .Test (sFile)   ' Execute search.
    End With
    Set oRegEx = Nothing
End Function
 
Sub RemoveFilesInFolder (sPath, bAtTop)
	' Recursive -- uses modular variables
	Dim objFSO
	Dim objFolder
	Dim objFolders
	Dim objFile
	Dim objFiles
	Dim objItem
	Dim sFileName
	Dim bPardoned
	Dim sWorkPath
	Dim iErr
	Dim aSort
 
	Set objFSO = CreateObject("Scripting.FileSystemObject")
	sWorkPath = sPath
	If Right (sWorkPath, 1) <> "\" Then sWorkPath = sWorkPath & "\"
 
	Set objFolder = objFSO.GetFolder(sPath)
 
	' First recurse to any sub-folders, and remove files from them.
	If bIncludeSubFolders Then
		For Each objItem In objFolder.SubFolders
			Call RemoveFilesInFolder (sWorkPath & objItem.Name, False)
		Next 'objItem
	End If
 
	WScript.Echo
	WScript.Echo "--- " & sPath
 
	' First, enumerate the files and delete them.
	If Not bAtTop Or Not bSpareRootFolder Then
		If objFolder.Files.Count - nPreserveFileCount > 0 Then
			ReDim aSort (objFolder.Files.Count - 1)
			nI = 0
			For Each objItem in objFolder.Files
				If nDateToCheck = 0 Then
					aSort (nI) = DateToAnsi (objItem.DateCreated) & "\" & objItem.Name
 
				ElseIf nDateToCheck = 1 Then
					aSort (nI) = DateToAnsi (objItem.DateLastModified) & "\" & objItem.Name
 
				ElseIf nDateToCheck = 2 Then
					aSort (nI) = DateToAnsi (objItem.DateLastAccessed) & "\" & objItem.Name
 
				End If
				nI = nI + 1
			Next
 
			Call QuickSort (aSort, Empty, Empty)
 
			For nI = 0 To UBound (aSort) - nPreserveFileCount
				sTmp = Split(aSort(nI), "\")(1)
				'WScript.Echo sTmp
				Set objFile = objFSO.GetFile (sWorkPath & sTmp)
 
				If Not FileMatchesPattern (objFile.Name, sFilePattern, True) Then
					bPardoned = True
					'WScript.Echo "Name does not match filter pattern: " & objFile.Name
 
				ElseIf bIgnoreZeroLengthFiles And objFile.Size = 0 Then
					bPardoned = True
					WScript.Echo "Ignoring null-length file: " & objFile.Name
 
				ElseIf Not bAggressive And (objFile.Attributes AND 7) <> 0 Then
					WScript.Echo "Attribute-protected file ignored: " & objFile.Name
					bPardoned = True
 
				ElseIf nDateToCheck = 0 And DateAdd ("d", 0, objFile.DateCreated) > dCutOff Then
					WScript.Echo "Young file kept: " & objFile.Name
					bPardoned = True
 
				ElseIf nDateToCheck = 1 And DateAdd ("d", 0, objFile.DateLastAccessed) > dCutOff Then
					WScript.Echo "Young file kept: " & objFile.Name
					bPardoned = True
 
				ElseIf nDateToCheck = 2 And DateAdd ("d", 0, objFile.DateLastModified > dCutOff) Then
					WScript.Echo "Young file kept: " & objFile.Name
					bPardoned = True
 
				ElseIf bTestRun Then
					WScript.Echo "Qualifies for deletion: " & objFile.Name
					bPardoned = True
 
				Else
					bPardoned = False
 
				End If
 
				If Not bPardoned Then
					Err.Clear
					sFileName = objFile.Name
					On Error Resume Next
					objFile.Delete
					iErr = Err.Number
					On Error Goto 0
					If iErr = 0 Then
						WScript.Echo "Deleted: " & objFile.Name
					Else
						WScript.Echo "Could not delete: " & objFile.Name
					End If
				End If
				Set objFile = Nothing
			Next ' File
		End If
	End If
 
	' Remove the folder if it is empty, and the option to remove empty folders has been selected.
	' NOTE: We don't delete the folder if any files are detected in it--protected or not.
	If (Not bAtTop Or bSpareRootFolder) And bRemoveEmptyFolders Then
 
		bPardoned = False
		For Each objItem In objFolder.Files
			bPardoned = True
			Exit For
		Next
		For Each objItem In objFolder.SubFolders
			bPardoned = True
			Exit For
		Next
 
		If Not bPardoned Then
			Err.Clear
			On Error Resume Next
			objFolder.Delete
			iErr = Err.Number
			On Error Goto 0
			If iErr = 0 Then
				WScript.Echo "Killed empty folder..."
			Else
				WScript.Echo "Empty folder survived (may contain hidden or system files)..."
			End If
		End If
	End If
 
	Set objItem = Nothing
	Set objFolder = Nothing
	Set objFSO = CreateObject("Scripting.FileSystemObject")
 
End Sub
 
Sub HandleError (iErr, sErr)
 
	If bBatchMode Then
		LogAnEvent 1, sMsg
	Else
		WScript.Echo sMsg
	End If
 
End Sub
 
Sub Usage ()
	WScript.Echo "DeleteOldFiles.vbs -- Remove files older than a specified number of days from one or more folders. "
	WScript.Echo "John J Schultz -- 12/12/2005 -- Public Domain. "
	WScript.Echo " "
	WScript.Echo "cscript DeleteOldFiles.vbs [{switches}] {days} folder [pattern]"
	WScript.Echo " "
	WScript.Echo "- Days is the age (in days) of the file's time stamp, to remove the file. Default: 30 days "
	WScript.Echo "- Folder is an existing folder containing the files targeted for removal. "
	WScript.Echo "- pattern is a regular expression used to filter matching file names targeted for removal. If not present, all files are included."
	WScript.Echo " "
	WScript.Echo "-------------------------- Available switches --------------------------- "
	WScript.Echo "/t   - test only (a.k.a. chicken run): shows files affected, but does not actually delete them."
	WScript.Echo "/a   - aggressive: attempts to remove read-only files. "
	WScript.Echo "/s   - recurse files in subdirectories. "
	WScript.Echo "/d   - remove a sub-directory if it is empty after removing files (does not include the initial folder). "
	WScript.Echo "/b   - run in batch mode (errors logged to NT Event Log) "
	WScript.Echo "/r   - spares files in the initial (root) folder.  Use with /s when only files in sub-folders "
	WScript.Echo "       should be removed.  If this switch is used without /s, it has no effect. "
	WScript.Echo "/0-  - ignores files with zero-length. "
	WScript.Echo "/d:c[reate]   - (default) uses creation date to measure age.  Note: Creation date on a copied "
	WScript.Echo "                file is the date it was copied (i.e.: it does not inherit the original file's "
	WScript.Echo "                creation date). "
	WScript.Echo "/d:a[ccess]   - uses last access date to measure age. "
	WScript.Echo "/d:m[odified] - uses last modified date to measure age. "
	WScript.Echo "/n:# - preserve a count of files. Leaves at least this number of the newest files in the folder."
	WScript.Echo "/l:n - Log to NT Event Log: n = chatter level: "
	WScript.Echo "       0 = single summary "
	WScript.Echo "       1 = start/completion "
	WScript.Echo "       2 = (1) and all failures "
	WScript.Echo "       3 = (2) and all successes "
	WScript.Echo "    NOTE: NT Event logging is performed if /b (batch mode) is specified, otherwise messages "
	WScript.Echo "          are directed to the console. "
	WScript.Echo "------------------------------------------------------------------------ "
	WScript.Echo " "
	WScript.Echo "//B is the switch recommended for production use with wscript to suppress operator messages from "
	WScript.Echo "the script engine (e.g. when launched from a timer).  Due to the number of WScript.Echo statements used, "
	WScript.Echo "CScript is highly recommended--even when testing. "
End Sub
 
' ###################################################################################
' MAIN
 
' Module
Dim oFSO
Dim oFolder
Dim oFile
Dim oArgs
Dim nArgCnt
Dim iCount
Dim sApp
Dim bValidParameters
Dim sTmp
Dim sMsg
 
sApp = "DeleteOldFiles.vbs"
 
bTestRun = False
bIncludeSubFolders = False
bRemoveEmptyFolders = False
bBenchmark = False
bUserConfirmationForDelete = False
bIgnoreZeroLengthFiles = False
bTestOnly = False
nDateToCheck = 2  ' Default to date modified.
nDays = -1
nPreserveFileCount = 0
sStartFolder = ""
sFilePattern = "[.]+"
 
bValidParameters = True
 
Set oArgs = WScript.Arguments
iCount = oArgs.Count
If iCount = 0 Then
	bValidParameters = False
End If
 
If bValidParameters Then
	For iCount = 0 to oArgs.Count - 1
		sTmp = LCase (LTrim (RTrim (oArgs(iCount))))
		If Left (sTmp, 1) = "/" Then
 
			If sTmp = "/a" Then
				bAggressive = True
 
			ElseIf sTmp = "/t" Then
				bTestRun = True
 
			ElseIf sTmp = "/s" Then
				bIncludeSubFolders = True
 
			ElseIf sTmp = "/r" Then
				bSpareRootFolder = True
 
			ElseIf sTmp = "/d" Then
				bRemoveEmptyFolders = True
 
			ElseIf sTmp = "/b" Then
				bBatchMode = False
 
			ElseIf sTmp = "/q" Then
				bUserConfirmationForDelete = True
 
			ElseIf sTmp = "/0-" Then
				bIgnoreZeroLengthFiles = True
 
			ElseIf Left (sTmp & Space (4), 4) = "/d:c" Then
				nDateToCheck = 0
 
			ElseIf Left (sTmp & Space (4), 4) = "/d:a" Then
				nDateToCheck = 1
 
			ElseIf Left (sTmp & Space (4), 4) = "/d:m" Then
				nDateToCheck = 2
 
			ElseIf Left (sTmp & Space (3), 3) = "/n:" Then
				nPreserveFileCount = Int (Mid (sTmp & Space (4), 4))
				If nPreserveFileCount < 0 Then
					WScript.Echo "Invalid preserve count: " & nPreserveFileCount
					bValidParameters = False
				End If
 
			Else
				bValidParameters = False
				sMsg = "unrecognized switch: " & sTmp
 
			End If
 
		ElseIf nDays = -1 And Instr ("0123456789", Left (sTmp, 1)) > 0 Then
			on error resume next
			nDays = CInt (sTmp)
			on error goto 0
			If nDays < 0 Then
				bValidParameters = False
				sMsg = "day count must be >= 0: " & sTmp
			End If
 
		ElseIf sStartFolder = "" Then
			sStartFolder = LTrim (RTrim (oArgs(iCount)))
 
		ElseIf sFilePattern = "[.]+" Then
			sFilePattern = LTrim (RTrim (oArgs(iCount)))
 
		Else
			bValidParameters = False
			sMsg = _
			"Attempt to re-specify the folder: " & sTmp & vbCrLf & _
			"Already set as: " & sStartFolder
		End If
	Next
End If
 
Set objArgs = Nothing
 
If sStartFolder = "" Then
	sMsg = _
	"No starting folder specified"
	bValidParameters = False
End If
 
If Not bValidParameters Then
	If bBatchMode Then
		LogAnEvent 1, sMsg
	Else
		WScript.Echo sMsg
	End If
	Call Usage()
	WScript.Quit (1)
End If
 
If nDays < 0 Then nDays = 30
 
dCutOff = DateAdd ("d", -nDays, Now ())
 
WScript.Echo "Removing files older than " & nDays & " days (cutoff time: " & dCutoff & ")"
WScript.Echo "Expiration is evaluated on the " & Split ("creation|last access|last modification", "|")(nDateToCheck) & " timestamp."
 
' --- Processing starts here
Call RemoveFilesInFolder (sStartFolder, True)
 
WScript.Echo "Script completed"
 
WScript.Quit(0)

04 Sep 2007
Blocking Dictionary Attacks on a Personal FTP site (IIS)

Thanks to some scripts I found on various sources, I was able to piece together this script to detect and block ever-increasing dictionary attacks against the Administrator account on my FTP server. Basically, you start the script using WScript at system load. It hooks itself into WMI events for ftp log file entries. When an entry is created, the log file is scanned for any IP address which has 5 or more bad login attempts in the log file. The code then checks to see if that IP has already been assigned the invalid gateway address, and adds the bad routing instruction for that IP if it has not already done so.

This is an enhancement to scripts I originally found at NetNerds.net and VisiMetrics.com, which provided the base code to create the dead route. I’ve added the following:

  • a sink to gracefully handle a Windows shutdown
  • a flag file located in the same folder as the script file, which will cause the script to terminate when deleted. It’s a way to kill a specific WScript process, instead of guessing which one of the umpteen WScript processes to remove in the task manager.
  • Logging to the NT Application Event Log when starting, stopping, or creating bad route entries for attackers.

All you need to do to make the script run is change the values assigned to three variables.

  • fakeGateWayIP = “192.168.1.200” ‘The black hole location in your sub-net to route the packets.
  • xMax = 5 ‘Max number of invalid login attempts
  • LogFiles = 10 ‘Max number of log files to keep in the folder before deleting them.

The beauty of this script is that it does not persist the bad routing instructions in other than the routing table, meaning that they are lost at the next reboot. If the attack occurs again, the bad route is just re-established after 5-10 attempts from the attacker. Truly a remarkably simple and effective technique. Thanks Chrissy and Tim!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
REM FtpBlocker.vbs
REM Launch once at system startup. Monitors the ftp activity for bad login attempts (Status Code 530).
REM If more than X failed login attempts are tried by a client, it's IP is added
REM to the routing table, with a fake gateway (dead IP) as the routing target. Designed to thwart
REM dictionary attacks.
REM This script works with Windows 2000 and later.
Dim aIpRoute, bShuttingDown
Function FileExists (sFileName)
	Dim oFSOSet oFSO = CreateObject("Scripting.FileSystemObject")
	FileExists = oFSO.FileExists(sFileName)
	set oFSO = Nothing
End Function
 
Sub DeleteFile (sFileName, bEvenIfReadOnly)
	Dim oFSO
 
	Set oFSO = CreateObject("Scripting.FileSystemObject")
	FileDelete = oFSO.DeleteFile(sFileName, bEvenIfReadOnly)
	set oFSO = Nothing
End Sub
 
Function FileToString (sFileName)
	Dim oFSO, oTS
	Set oFSO = CreateObject("Scripting.FileSystemObject")
	Set oTS = oFSO.OpenTextFile(sFileName, 1)
 
	FileToString = oTS.ReadAll
	oTS.Close()
 
	set oTS = Nothing
	set oFSO = Nothing
End Function
 
Sub StringToFile (sFileName, sText)
	Dim oFSO, oTS
	Set oFSO = CreateObject("Scripting.FileSystemObject")
	Set oTS = oFSO.OpenTextFile(sFileName, 2, True)
 
	oTS.Write sText
	oTS.Close()
 
	set oTS = Nothing
	set oFSO = Nothing
End Sub
 
Function RunOutput(cProgram, nWindowType)
	REM -- Obtain a Temporary File Name
	Dim oFS
	Set oFS = CreateObject("Scripting.FileSystemObject")
	Dim cFile
	cFile = oFS.GetSpecialFolder(2).Path & "\" & oFS.GetTempName
 
	REM -- Execute the command and redirect the output to the file
	Dim oShell
	Set oShell = CreateObject( "WScript.Shell" )
	oShell.Run cProgram & " >" & cFile, nWindowType, True
	Set oShell = Nothing
 
	REM -- Read output file and return
	Dim oFile
	Set oFile = oFS.OpenTextFile(cFile, 1, True)
	RunOutput = oFile.ReadAll()
	oFile.Close
 
	REM -- Delete Temporary File
	oFS.DeleteFile cFile
	Set oFS = Nothing
End Function
 
Function GetRoutingEntries()
	REM Capture the content of the current routing table.
	aRaw = Split (RunOutput("%COMSPEC% /C ROUTE PRINT", 0), vbCrLf)
 
	Set o = new RegExp
	o.Pattern = "((\d)+\.){3}(\d)+"
	o.Global = True
	o.MultiLine = False
	o.IgnoreCase = False
	aIP = Empty
	For Each s In aRaw
		Set oMatches = o.Execute (s)
		If oMatches.Count = 4 Then
			If IsArray (aIP) Then
				ReDim Preserve aIP (UBound (aIP, 1) + 1)
			Else
				ReDim aIP (0)
			End If
			aIP (UBound (aIP, 1)) = oMatches.Item(0) & "|" & oMatches.Item(2)
		End If
	Next
	GetRoutingEntries = aIP
End Function
 
REM -- Log the startup to the event log.
Set oWsh = WScript.CreateObject("WScript.Shell")
oWsh.LogEvent 0, "FtpBlocker530.vbs begins"
Set oWsh = WScript.CreateObject("WScript.Shell")
 
REM Load a snapshot of the IP's in the routing table, and use this as the baseline for determining
REM if the IP is in the list already.
 
aIpRoute = GetRoutingEntries()
bShuttingDown = False
 
REM Push Event Viewer Alert
Set objWMIService = GetObject("winmgmts:{(security)}!root/cimv2")
Set eventSink = wscript.CreateObject("WbemScripting.SWbemSink", "EVSINK100_")
strWQL = "Select * from __InstanceCreationEvent where TargetInstance isa 'Win32_NTLogEvent' and TargetInstance.SourceName = 'MSFTPSVC' and TargetInstance.EventCode = 100"
objWMIService.ExecNotificationQueryAsync eventSink,strWQL
 
Set objWMIService1 = GetObject("winmgmts:{(security)}!root/cimv2")
Set eventSink1 = wscript.CreateObject("WbemScripting.SWbemSink", "EVSINK6006_")
strWQL = "Select * from __InstanceCreationEvent where TargetInstance isa 'Win32_NTLogEvent' and TargetInstance.SourceName = 'eventlog' and TargetInstance.EventCode = 6006"
objWMIService.ExecNotificationQueryAsync eventSink1,strWQL
 
REM Create a flag file named FtpBlocker530.flg
REM When this file is not on the system, it is a signal to terminate this application.
 
StringToFile "FtpBlocker530.flg", "*** MARKER FILE ONLY ***"
 
REM Keep it going forever, or until the marker file is missing.
While (FileExists ("FtpBlocker530.flg") And Not bShuttingDown)
	Wscript.Sleep(1000)
Wend
 
If Not bShuttingDown Then
	Set oWsh = WScript.CreateObject("WScript.Shell")
	oWsh.LogEvent 0, "FtpBlocker530.vbs stopped by file removal"
	Set oWsh = Nothing
Else
	DeleteFile "FtpBlocker530.flg", True
End If
Set eventSink = Nothing
Set eventSink1 = Nothing
Set objWMIService = Nothing
Set objWMIService1 = Nothing
 
Sub EVSINK6006_OnObjectReady(objObject, objAsyncContext)
	bShuttingDown = True
End Sub
 
Sub EVSINK100_OnObjectReady(objObject, objAsyncContext)
 
	Set objDictionary = CreateObject("Scripting.Dictionary")
	Set objFSO = CreateObject("Scripting.FileSystemObject")
	Set objLog = CreateObject("MSWC.IISLog")
	Set WshShell = WScript.CreateObject("WScript.Shell")
 
	fakeGateWayIP = "192.168.1.200" 'This is a black hole location on your subnet to route the packets.
	xMax = 5 'Max number of invalid login attempts
	xLogFiles = 10 'Max number of log files to keep in the folder before deleting them
 
	Set objFolder = objFSO.GetFolder(objFSO.GetSpecialFolder(0).Path & "\system32\LogFiles\MSFTPSVC1\")
	Set objFiles = objFolder.Files
	For Each fileName In objFiles
		lastFile = fileName
		Set f = objFSO.GetFile(fileName)
		If f.DateCreated <= Date - xLogFiles Then objFSO.DeleteFile FileName, True
		Set f = Nothing
	Next
	Set objFiles = Nothing
	Set objFolder = Nothing
 
	objLog.OpenLogFile lastFile, 1, "MSFTPSVC", 1, 0
	While NOT objLog.AtEndOfLog
		objLog.ReadLogRecord
		clientIP = trim(objLog.ClientIP)
		xStatus = trim(objLog.ProtocolStatus)
		If xStatus = "530" AND NOT (clientIP = fakeGateWayIP) then
			If objDictionary.Exists(ClientIP) Then
				objDictionary.Item(clientIP) = cStr(Clng(objDictionary.Item(clientIP)) + 1)
			Else
				objDictionary.Add clientIP,"1"
			End If
		End If
	Wend
	objLog.CloseLogFiles 1
 
	xChange = False
	For Each xClient in objDictionary.Keys
		xTest = False
		If Clng(objDictionary.Item(xClient)) < xMax then
			objDictionary.Remove(xClient)
		Else
			xTest = Not IsArray (aIpRoute)
			If Not xTest Then
				xTest = True
				For Each ip In aIpRoute
					If Split (ip, "|")(0) = xClient And Split (ip, "|")(1) = fakeGateWayIP Then
						xTest = False ' Client already exists in the routing table with the fakeGateWayIP as the gateway
						Exit For
					End If
				Next
			End If
			If xTest Then
				xChange = True
				WshShell.Run "%COMSPEC% /C ROUTE ADD " & xClient & " MASK 255.255.255.255 " & fakeGateWayIP, 0, True
				WshShell.LogEvent 1, "BAD ROUTE(s) assigned: FTP max of " & xMax & " occurrences for 530 Event(s) exceeded for " & xClient
			End If
		End If
	Next
 
	Set WshShell = Nothing
	Set objLog = Nothing
	Set objFSO = Nothing
	Set objDictionary = Nothing
 
	If xChange Then
		aIpRoute = GetRoutingEntries()
	End If
End Sub