|
1 package AntiVirus; |
|
2 use strict; |
|
3 |
|
4 use File::Copy; |
|
5 use Win32::Process; |
|
6 |
|
7 require Exporter; |
|
8 our @ISA = qw(Exporter); |
|
9 our @EXPORT_OK = qw{Start Stop Scan WaitTillAntiVirusStarts}; |
|
10 |
|
11 # Start: |
|
12 # Description: attempts to start all supported anti-virus services/processes |
|
13 # Revised April 2007 to support McAfee only, Sophos and WebRoot having passed into history! |
|
14 # Arguments: none |
|
15 # Returns: none |
|
16 sub Start() |
|
17 { |
|
18 my @iMcAfeeCommands = ( |
|
19 {AppName => 'net.exe', Params => 'start McAfeeFramework'}, |
|
20 {AppName => 'net.exe', Params => 'start McShield'}, |
|
21 {AppName => 'net.exe', Params => 'start McTaskManager'}, |
|
22 # Must supply full pathname, as these applications are not in "PATH" |
|
23 {AppName => 'C:\\Program Files\\Network Associates\\Common Framework\\UpdaterUI.exe', Params => '/StartedFromRunKey'}, |
|
24 {AppName => 'C:\\Program Files\\Network Associates\\Common Framework\\UdaterUI.exe', Params => '/StartedFromRunKey'}, |
|
25 {AppName => 'C:\\Program Files\\McAfee\\Common Framework\\UpdaterUI.exe', Params => '/StartedFromRunKey'}, |
|
26 {AppName => 'C:\\Program Files\\McAfee\\Common Framework\\UdaterUI.exe', Params => '/StartedFromRunKey'}, |
|
27 {AppName => 'C:\\Program Files\\Common Files\\Network Associates\\TalkBack\\tbmon.exe', Params => ''}, |
|
28 {AppName => 'C:\\Program Files\\Network Associates\\VirusScan\\SHSTAT.EXE', Params => '/STANDALONE'}, |
|
29 {AppName => 'C:\\Program Files\\McAfee\\VirusScan\\SHSTAT.EXE', Params => '/STANDALONE'}); |
|
30 |
|
31 # Execute all "START" commands |
|
32 for my $iCmd(@iMcAfeeCommands) |
|
33 { |
|
34 ExecuteProcess(20,$iCmd); |
|
35 } |
|
36 |
|
37 } |
|
38 |
|
39 # WaitTillAntiVirusStarts |
|
40 # Description: |
|
41 # Sometimes the Antivirus::Stop() is invoked before the Antivirus services are started in the machine. |
|
42 # because of which the Antivirus starts when the build is ongoing, and the active Antivirus disrupts/slows down the build |
|
43 # |
|
44 # If the Antivirus services fail to start beyond the specified timeout period,reports error and aborts the build. |
|
45 # |
|
46 # Arguments : Waiting time in seconds before next attempt is made to check AV service status, max number of attempts |
|
47 # Returns : returns nothing if AV running, aborts build if AV not running even after the waiting period |
|
48 sub WaitTillAntiVirusStarts{ |
|
49 my $waitingTime = shift; |
|
50 my $retries = shift; |
|
51 |
|
52 my @waitList = shift; |
|
53 |
|
54 my $defaultWaitingTime = 30; # Waiting time in seconds, before next attempt is made to check AV service status |
|
55 my $defaultRetries = 5; # Try upto 5 times |
|
56 |
|
57 # assign default values to waitingtime,retries if values not specified |
|
58 unless($waitingTime) |
|
59 { |
|
60 $waitingTime = $defaultWaitingTime; |
|
61 } |
|
62 unless($retries) |
|
63 { |
|
64 $retries = $defaultRetries; |
|
65 } |
|
66 |
|
67 my $attempt = 0; |
|
68 ## If AV not active, wait and retry |
|
69 while(1) |
|
70 { |
|
71 if(IsAntiVirusActive(@waitList)) |
|
72 { |
|
73 return; ## AV is active, all fine, proceed |
|
74 } |
|
75 $attempt++; |
|
76 if($attempt > $retries) |
|
77 { |
|
78 last; |
|
79 } |
|
80 print "REMARK: One or more Antivirus services not active yet (At ".scalar localtime().") \n"; |
|
81 print "Waiting $waitingTime secs before rechecking status (Attempt $attempt of $retries) ...\n\n"; |
|
82 sleep($waitingTime); |
|
83 } |
|
84 |
|
85 ## Antivirus is not active even after waiting $waitingTime secs $retries times, report error and abort the build |
|
86 |
|
87 print "ERROR: RealTimeBuild : Antivirus is not active even after waiting for $waitingTime secs x $retries attempts\n"; |
|
88 print "Antivirus cannot be reliably stopped,abandon build."; |
|
89 } |
|
90 |
|
91 |
|
92 # IsAntiVirusActive |
|
93 # Description: |
|
94 # Runs 'net start' command to get a list of active services,checks if the Antivirus services are in the list |
|
95 # |
|
96 # Arguments : None |
|
97 # Returns : |
|
98 # returns 0 even if one Antivirus Service is inactive |
|
99 # returns 1 if All AntiVirus Services active |
|
100 sub IsAntiVirusActive{ |
|
101 |
|
102 my @waitList = shift; |
|
103 |
|
104 my @AntiVirusServices = @waitList; |
|
105 |
|
106 print "Checking Antivirus services status ...\n"; |
|
107 my $iCmd={AppName => 'net.exe', Params => 'start'}; |
|
108 |
|
109 # Check if net.exe is accessible |
|
110 unless(-e $iCmd->{'AppName'}) |
|
111 { |
|
112 my $iExecutable = FindFileFromPath($iCmd->{'AppName'}); |
|
113 unless ($iExecutable) |
|
114 { |
|
115 print "REMARK: File not found $iCmd->{'AppName'}\n"; |
|
116 print "REMARK: Unable to check Antivirus status\n"; |
|
117 return 0; |
|
118 } |
|
119 $iCmd->{'AppName'} = $iExecutable; |
|
120 } |
|
121 |
|
122 my $iCommand = "\"$iCmd->{'AppName'}\" $iCmd->{'Params'}" ; |
|
123 print "Running: $iCommand\n"; |
|
124 my @iOutput = `$iCommand`; |
|
125 |
|
126 return &ParseServiceStatus(\@iOutput,\@AntiVirusServices); |
|
127 } |
|
128 |
|
129 |
|
130 # ParseServiceStatus |
|
131 # Description: |
|
132 # Runs Compares output of 'net start' command to get a list of active services,checks if the Antivirus services are in the list |
|
133 # |
|
134 # Arguments : Output of 'net start', Antivirus service list |
|
135 # Returns : |
|
136 # returns 0 even if one Antivirus Service is not in the list |
|
137 # returns 1 if All AntiVirus Services are in the list |
|
138 sub ParseServiceStatus($$) |
|
139 { |
|
140 my $iOutputRef = shift; |
|
141 my $iAntiVirusServiceList = shift; |
|
142 foreach(@$iOutputRef) |
|
143 { |
|
144 chomp ; |
|
145 s/^[\t\s]+//g; # strip whitespace at the beginning of the line |
|
146 } |
|
147 |
|
148 for my $serviceName(@$iAntiVirusServiceList) |
|
149 { |
|
150 my @match = grep (/^$serviceName$/i, @$iOutputRef); |
|
151 unless(@match) |
|
152 { |
|
153 print "Antivirus Service \'$serviceName\' inactive\n"; |
|
154 return 0; |
|
155 } |
|
156 } |
|
157 print "All Antivirus services active.\n"; |
|
158 return 1; |
|
159 } |
|
160 |
|
161 |
|
162 # Stop: |
|
163 # Description: attempts to stop all supported anti-virus services/processes |
|
164 # Revised April 2007 to support McAfee only, Sophos and WebRoot having passed into history! |
|
165 # Arguments: none |
|
166 # Returns: none |
|
167 sub Stop($$) |
|
168 { |
|
169 # Need to check if the Antivirus services is installed or enabled. |
|
170 my @iMcAfeeServices = ( |
|
171 {AppName => 'net.exe', Params => 'stop McAfeeFramework', Services =>'McAfee Framework Service'}, |
|
172 {AppName => 'net.exe', Params => 'stop McShield', Services =>'Network Associates McShield'}, |
|
173 {AppName => 'net.exe', Params => 'stop McTaskManager', Services =>'Network Associates Task Manager'}); |
|
174 |
|
175 my @iPskillCommands = ( |
|
176 {AppName => 'pskill.exe', Params => 'UpdaterUI.exe'}, |
|
177 {AppName => 'pskill.exe', Params => 'tbmon.exe'}, |
|
178 {AppName => 'pskill.exe', Params => 'shstat.exe'}); |
|
179 |
|
180 my @waitList = (); |
|
181 my @iMcAfeeCommands = (); |
|
182 |
|
183 for my $iTestCmd(@iMcAfeeServices) |
|
184 { |
|
185 my $iExecutable = $iTestCmd->{'AppName'}; |
|
186 my $iParams = $iTestCmd->{'Params'}; |
|
187 my $iServices = $iTestCmd->{'Services'}; |
|
188 |
|
189 my @testCommand = `$iExecutable $iParams 2>&1`; |
|
190 my $iResponse = 0; |
|
191 |
|
192 foreach my $iLine (@testCommand) |
|
193 { |
|
194 if ($iLine =~ m/does not exist/i) |
|
195 { |
|
196 print "REMARK: $iServices not installed, just proceed.\n"; |
|
197 $iResponse = 1; |
|
198 } |
|
199 |
|
200 if ($iLine =~ m/it is disabled/i) |
|
201 { |
|
202 print "REMARK: $iServices not enabled, just proceed.\n"; |
|
203 $iResponse = 1; |
|
204 } |
|
205 |
|
206 if (($iLine =~ m/service is not started/i ) or ($iLine =~ m/not valid for this service/i )) |
|
207 { |
|
208 print "REMARK: $iServices not started, just wait.\n"; |
|
209 push @waitList, $iServices; |
|
210 push @iMcAfeeCommands, {AppName =>$iExecutable, Params =>$iParams}; |
|
211 $iResponse = 1; |
|
212 } |
|
213 |
|
214 if ($iLine =~ m/service was stopped successfully/i) |
|
215 { |
|
216 print "REMARK: Stop Success! $iServices was stopped successfully directly by the test command, just proceed.\n"; |
|
217 $iResponse = 1; |
|
218 } |
|
219 |
|
220 if ($iLine =~ m/service could not be stopped/i) |
|
221 { |
|
222 print "REMARK: Stop Success! $iServices could not be stopped at the moment but will be stopped successfully directly by the test command very soon later, just proceed.\n"; |
|
223 $iResponse = 1; |
|
224 } |
|
225 |
|
226 if ($iLine =~ m/System error 5 has occurred/i) |
|
227 { |
|
228 print "WARNING: Access to $iServices is denied, but this won't affect the build, just proceed.\n"; |
|
229 $iResponse = 1; |
|
230 } |
|
231 |
|
232 } |
|
233 unless ($iResponse) |
|
234 { |
|
235 print "ERROR: Unable to parse the output of $iExecutable for $iParams!\n"; |
|
236 foreach my $iLine (@testCommand) |
|
237 { |
|
238 print $iLine; |
|
239 } |
|
240 #push @waitList, $iServices; |
|
241 #push @iMcAfeeCommands, {AppName =>$iExecutable, Params =>$iParams}; |
|
242 } |
|
243 } |
|
244 |
|
245 # Wait for the waiting Antivirus services to load before attempting to stop the services |
|
246 # If Antivirus fails to load after the grace period, the build should be aborted. |
|
247 my $gWaitingTime = shift; |
|
248 my $gRetries = shift; |
|
249 if (@waitList) |
|
250 { |
|
251 WaitTillAntiVirusStarts($gWaitingTime, $gRetries, @waitList); |
|
252 } |
|
253 |
|
254 # Execute all "STOP" commands |
|
255 push @iMcAfeeCommands, @iPskillCommands; |
|
256 for my $iCmd(@iMcAfeeCommands) |
|
257 { |
|
258 ExecuteProcess(20,$iCmd); |
|
259 } |
|
260 } |
|
261 |
|
262 # Scan: |
|
263 # Description: performs the virus scan on the specified directores |
|
264 # Revised April 2007 to support McAfee only, Sophos having passed into history! |
|
265 # Revised February 2008 to support further McAfee filename changes. |
|
266 # Arguments: directory in which to place scan output file (logfile); |
|
267 # array ref to array containing a list of the directories to scan |
|
268 # Returns: none |
|
269 sub Scan($$) |
|
270 { |
|
271 my $iOutFilesDir = shift; |
|
272 my $iDirsToScan = shift; |
|
273 |
|
274 # Name(s) of output files. User only supplies directories |
|
275 my $iOutFileName = 'Anti-virus'; |
|
276 |
|
277 # Define McAfee commands, newest version first. McAfee keep changing the names of their files. Thus if an |
|
278 # executable is not found, we do not treat this as an error, we simply go on to thwe next command definition. |
|
279 # Note: Must supply full pathname, as these applications are not in "PATH" |
|
280 my @iMcAfeeCommands =( |
|
281 # CSScan.exe send most of its output to STDOUT. So define file a second time for us to write this data |
|
282 {AppName => 'C:\\Program Files\\McAfee\\VirusScan Enterprise\\csscan.exe', |
|
283 Params => join(" ", @$iDirsToScan) . " /SUB /CLEAN /ALLAPPS /LOG $iOutFilesDir\\$iOutFileName.log", |
|
284 LogFile => "$iOutFilesDir\\$iOutFileName"}, # Filename without .LOG extension |
|
285 {AppName => 'C:\\Program Files\\Common Files\\Network Associates\\Engine\\scan.exe', |
|
286 Params => join(" ", @$iDirsToScan) . " /SUB /NOBREAK /CLEAN /NORENAME /RPTCOR /RPTERR /REPORT=$iOutFilesDir\\$iOutFileName.log", |
|
287 LogFile => ''} # The older "SCAN.EXE" writes everything to its report file. So no further logging required. |
|
288 ); |
|
289 |
|
290 print "Scanning: @$iDirsToScan\n"; |
|
291 |
|
292 # Execute "SCAN" commands in order until one is successful. |
|
293 for my $iCmd(@iMcAfeeCommands) |
|
294 { |
|
295 # First remove/rename any existing output file. This is an unlikely situation in a normal build; |
|
296 # but it could cause confusion if, for example, a scan was re-run manually. |
|
297 unless (RemoveExistingFile($iCmd->{'LogFile'})) { next; } |
|
298 if (RunCommand($iCmd)) { return; } # Success |
|
299 } |
|
300 # Arriving at the end of this loop means that no command succeeded! Report failure! |
|
301 print "WARNING: No Virus Scan accomplished. Possibly no suitable executable found.\n"; |
|
302 } |
|
303 |
|
304 # RunCommand: |
|
305 # Description: Runs specified command using Perl "backticks" |
|
306 # passing its output for parsing to the sub ParseOutput(). |
|
307 # Arguments: the command to execute as a hashref (filename of executable and parameters/args to pass to it) |
|
308 # Returns: TRUE on success |
|
309 sub RunCommand($) |
|
310 { |
|
311 my $iCmd = shift; # Hashref - Command spec. |
|
312 |
|
313 unless(-e $iCmd->{'AppName'}) |
|
314 { |
|
315 print "REMARK: File not found $iCmd->{'AppName'}\n"; |
|
316 return 0; # FALSE = Failure |
|
317 } |
|
318 my $iCommand = "\"$iCmd->{'AppName'}\" $iCmd->{'Params'}" ; |
|
319 print "Running: $iCommand\n"; |
|
320 my @iOutput = `$iCommand`; |
|
321 |
|
322 # Parse the output flagging any errors |
|
323 return &ParseOutput(\@iOutput,$iCmd->{'LogFile'}); # Return TRUE or FALSE |
|
324 } |
|
325 |
|
326 # ParseOutput: |
|
327 # Description: Parse output for warnings and errors. |
|
328 # The ScanLog-compatible "WARNING: " is prepended to any line containing |
|
329 # any of these messages or errors, and all lines are printed to STDOUT. |
|
330 # Arguments: reference to the array of output lines from executed command; Name of logfile (if any) |
|
331 # Returns: TRUE on Success |
|
332 # Remarks: note that errors in starting/stopping processes or services do not |
|
333 # constitute errors in terms of the build process. Failure to start or stop |
|
334 # the services will not affect the compilation except, at worst, to slow it |
|
335 # down if stopping the antivirus fails. |
|
336 sub ParseOutput($) |
|
337 { |
|
338 my $iOutputRef = shift; |
|
339 my $iLogFile = shift; |
|
340 |
|
341 my $fh = \*LOGFILE; |
|
342 |
|
343 if($iLogFile) |
|
344 { |
|
345 unless (open $fh, ">>$iLogFile.log") |
|
346 { |
|
347 print ("WARNING: Failed to open log file: $iLogFile"); |
|
348 return 0; # FALSE = Failure |
|
349 } |
|
350 } |
|
351 else |
|
352 { |
|
353 $fh = \*STDOUT; |
|
354 } |
|
355 |
|
356 for my $line(@$iOutputRef) # For each line of output... |
|
357 { |
|
358 $line =~ s/\s+$//; # Remove trailing spaces (McAfee CSSCAN pads each line to 1024 chars!!) |
|
359 # Does it match an error as returned by PSKill or net stop/start |
|
360 if($line =~ /The specified service does not exist as an installed service\./i or |
|
361 $line =~ /The .*? service is not started\./i or |
|
362 $line =~ /The requested service has already been started./i or |
|
363 $line =~ /The service name is invalid\./i or |
|
364 $line =~ /Process does not exist\./i or |
|
365 $line =~ /Unable to kill process/i or |
|
366 $line =~ /error/i or |
|
367 $line =~ /is not recognized as an internal or external command/i) |
|
368 { |
|
369 print "WARNING: $line\n"; |
|
370 } |
|
371 else # No errors/warnings so just print the line on its own |
|
372 { |
|
373 print $fh "$line\n"; |
|
374 } |
|
375 } |
|
376 return 1; # TRUE = Success |
|
377 } |
|
378 |
|
379 # ExecuteProcess: |
|
380 # Description: Executes specified command using Perl module Win32::Process |
|
381 # Arguments: the command to execute as a hashref (filename of executable and parameters/args to pass to it) |
|
382 # Returns: none |
|
383 sub ExecuteProcess |
|
384 { |
|
385 my $iWaitSecs = shift; |
|
386 my $iCmd = shift; # Hashref - Command spec. |
|
387 |
|
388 my $iExecutable = $iCmd->{'AppName'}; |
|
389 unless(-e $iExecutable) |
|
390 { |
|
391 $iExecutable = FindFileFromPath($iCmd->{'AppName'}); |
|
392 unless ($iExecutable) |
|
393 { |
|
394 print "REMARK: File not found $iCmd->{'AppName'}\n"; |
|
395 return; |
|
396 } |
|
397 } |
|
398 my $iParams = $iCmd->{'Params'}; |
|
399 |
|
400 # my $iFlags = $iDebug? CREATE_NEW_CONSOLE: DETACHED_PROCESS; |
|
401 # my $iFlags = $iDebug? 0: DETACHED_PROCESS; |
|
402 my $iFlags = 0; |
|
403 |
|
404 # Create a new Perl process (because fork does not work on some versions of Perl on Win32) |
|
405 # $^X is the path to the Perl binary used to process this script |
|
406 my $iProcess; |
|
407 unless (Win32::Process::Create($iProcess, "$iExecutable", "\"$iExecutable\" $iParams", 0, $iFlags, ".")) |
|
408 { |
|
409 print "WARNING: Failed to create process for $iExecutable $iParams.\n"; |
|
410 my $iExitCode = Win32::GetLastError(); |
|
411 ReportProcessError($iExitCode); |
|
412 return; |
|
413 } |
|
414 |
|
415 my $iPID = $iProcess->GetProcessID(); |
|
416 print "\nExecuting: $iExecutable $iParams .....\n"; |
|
417 my $iRetVal = $iProcess->Wait($iWaitSecs * 1000); # milliseconds. Return value is zero on timeout, else 1. |
|
418 if ($iRetVal == 0) # Wait timed out |
|
419 { # No error from child process (so far) |
|
420 print "Spawned: $iExecutable $iParams - PID=$iPID.\n"; |
|
421 return 1; # Success |
|
422 } |
|
423 else # Child process terminated. Wait usually returns 1. |
|
424 { # Error in child process?? If so, get exit code |
|
425 my $iExitCode; |
|
426 $iProcess->GetExitCode($iExitCode); |
|
427 unless($iExitCode) |
|
428 { |
|
429 print "Executed: $iExecutable $iParams - PID=$iPID.\n"; |
|
430 return 1; # Success |
|
431 } |
|
432 print "REMARK: Failed in execution of $iExecutable $iParams.\n"; |
|
433 ReportProcessError($iExitCode); |
|
434 return 0; |
|
435 } |
|
436 } |
|
437 |
|
438 # ReportProcessError: |
|
439 # Description: prints error code to STDOUT followed by explanatory text, if avalable |
|
440 # Arguments: Windows error code |
|
441 # Returns: none |
|
442 sub ReportProcessError |
|
443 { |
|
444 my $iExitCode = shift; |
|
445 |
|
446 my $iMsg = Win32::FormatMessage($iExitCode); |
|
447 if (defined $iMsg) |
|
448 { |
|
449 printf "ExitCode: 0x%04x = %s\n", $iExitCode, $iMsg; |
|
450 } |
|
451 else |
|
452 { |
|
453 printf "ExitCode: 0x%04x\n", $iExitCode; |
|
454 } |
|
455 } |
|
456 |
|
457 # FindFileFromPath: |
|
458 # Description: Tries to find a file using PATH Environment Variable |
|
459 # Argument: Filename (Must include extension .EXE, .CMD etc) |
|
460 # Return: Full pathname of file, if found, otherwise undef |
|
461 sub FindFileFromPath |
|
462 { |
|
463 my $iFilename = shift; |
|
464 my $iPathname; |
|
465 my @iPathDirs = split /;/, $ENV{'PATH'}; |
|
466 foreach my $iDir (@iPathDirs) |
|
467 { |
|
468 while ($iDir =~ s/\\$//){;} # Remove any trailing backslash(es) |
|
469 $iPathname = "$iDir\\$iFilename"; |
|
470 if (-e $iPathname) |
|
471 { |
|
472 return $iPathname; |
|
473 } |
|
474 } |
|
475 return undef; |
|
476 } |
|
477 |
|
478 # RemoveExistingFile: |
|
479 # Description: If specified .LOG file exists, rename to .BAK |
|
480 # Arguments: File to check |
|
481 # Returns: TRUE on Success |
|
482 sub RemoveExistingFile |
|
483 { |
|
484 my $iFilename = shift; # Full pathname LESS extension |
|
485 |
|
486 unless (-e "$iFilename.log") { return 1; } # Success! |
|
487 |
|
488 if (File::Copy::move("$iFilename.log","$iFilename.bak")) { return 1; } # Success! |
|
489 |
|
490 print "WARNING: Failed to rename $iFilename.log\nto $iFilename.bak because of:\n$!\n"; |
|
491 return 0; # FALSE = Failure |
|
492 } |
|
493 |
|
494 1; |