managementnode/lib/VCL/Module/OS/Windows/Version_6.pm (1,487 lines of code) (raw):

#!/usr/bin/perl -w ############################################################################### # $Id$ ############################################################################### # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ############################################################################### =head1 NAME VCL::Module::OS::Windows::Version_6.pm - VCL module to support Windows 6.x operating systems =head1 SYNOPSIS Needs to be written =head1 DESCRIPTION This module provides VCL support for Windows version 6.x operating systems. Version 6.x Windows OS's include Windows Vista, Windows Server 2008, and Windows 7. =cut ############################################################################### package VCL::Module::OS::Windows::Version_6; # Specify the lib path using FindBin use FindBin; use lib "$FindBin::Bin/../../../.."; # Configure inheritance use base qw(VCL::Module::OS::Windows); # Specify the version of this module our $VERSION = '2.5.1'; # Specify the version of Perl to use use 5.008000; use strict; use warnings; use diagnostics; use VCL::utils; use File::Basename; ############################################################################### =head1 CLASS VARIABLES =cut =head2 $SOURCE_CONFIGURATION_DIRECTORY Data type : Scalar Description : Location on management node of script/utilty/configuration files needed to configure the OS. This is normally the directory under the 'tools' directory specific to this OS. =cut our $SOURCE_CONFIGURATION_DIRECTORY = "$TOOLS/Windows_Version_6"; ############################################################################### =head1 INTERFACE OBJECT METHODS =cut #////////////////////////////////////////////////////////////////////////////// =head2 pre_capture Parameters : None Returns : If successful: true If failed: false Description : Performs steps before an image is captured which are specific to Windows version 6.x. =over 3 =cut sub pre_capture { my $self = shift; my $args = shift; # Check if subroutine was called as an object method unless (ref($self) && $self->isa('VCL::Module')) { notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a VCL::Module object method"); return; } =item * Call parent class's pre_capture() subroutine =cut notify($ERRORS{'OK'}, 0, "calling parent class pre_capture() subroutine"); if ($self->SUPER::pre_capture($args)) { notify($ERRORS{'OK'}, 0, "successfully executed parent class pre_capture() subroutine"); } else { notify($ERRORS{'WARNING'}, 0, "failed to execute parent class pre_capture() subroutine"); return 0; } notify($ERRORS{'OK'}, 0, "beginning Windows version 6 image pre-capture tasks"); =item * Disable the following scheduled tasks: * ScheduledDefrag - This task defragments the computers hard disk drives * SR - This task creates regular system protection points * Consolidator - If the user has consented to participate in the Windows Customer Experience Improvement Program, this job collects and sends usage data to Microsoft =cut my @scheduled_tasks = ( '\Microsoft\Windows\Defrag\ScheduledDefrag', '\Microsoft\Windows\SystemRestore\SR', '\Microsoft\Windows\Customer Experience Improvement Program\Consolidator', ); for my $scheduled_task (@scheduled_tasks) { $self->disable_scheduled_task($scheduled_task); } =item * Deactivate Windows licensing activation =cut if (!$self->deactivate()) { notify($ERRORS{'WARNING'}, 0, "unable to deactivate Windows licensing activation"); return 0; } =back =cut notify($ERRORS{'OK'}, 0, "returning 1"); return 1; } ## end sub pre_capture #////////////////////////////////////////////////////////////////////////////// =head2 post_load Parameters : None Returns : If successful: true If failed: false Description : Performs steps after an image is loaded which are specific to Windows version 6.x. =over 3 =cut sub post_load { my $self = shift; # Check if subroutine was called as an object method unless (ref($self) && $self->isa('VCL::Module')) { notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a VCL::Module object method"); return; } =item * Call parent class's post_load() subroutine =cut $self->SUPER::post_load() || return; notify($ERRORS{'DEBUG'}, 0, "beginning Windows version 6 post-load tasks"); =item * Ignore default routes configured for the private interface and use default routes configured for the public interface =cut $self->set_ignore_default_routes(); =item * Activate Windows license =cut $self->activate(); =back =cut notify($ERRORS{'DEBUG'}, 0, "Windows version 6 post-load tasks complete"); return 1; } ############################################################################### =head1 AUXILIARY OBJECT METHODS =cut #////////////////////////////////////////////////////////////////////////////// =head2 activate Parameters : None Returns : If successful: true If failed: false Description : Activates Microsoft Windows. A first attempt is made using a MAK key if one has been configured in the winProductKey table for the version of Windows installed on the computer. If unable to activate using a MAK key, activation is attempting using a KMS server configured in the winKMS table. =cut sub activate { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } ## Check if Windows has already been activated #my $license_status = $self->get_license_status(); #if ($license_status && $license_status =~ /licensed/i) { # notify($ERRORS{'OK'}, 0, "Windows has already been activated"); # return 1; #} # Attempt to activate first using KMS server # Attempt to activate using MAK if KMS fails or is not configured if ($self->activate_kms() || $self->activate_mak()) { return 1; } else { # Display the computer's current time in vcld.log to help diagnose the problem # Activation fails if the client's time is incorrect $self->get_current_computer_time('after activation failed'); notify($ERRORS{'CRITICAL'}, 0, "failed to activate Windows using MAK or KMS methods"); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 activate_mak Parameters : None Returns : If successful: true If failed: false Description : Attempts to activate Windows using a MAK key stored in the winProductKey table. =cut sub activate_mak { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Attempt to get the product key stored in the winProductKey table # This will return the correct key for the affiliation and version of Windows installed on the computer my $product_key = $self->get_product_key(); if ($product_key) { notify($ERRORS{'DEBUG'}, 0, "retrieved MAK product key from the winProductKey table: $product_key"); } else { notify($ERRORS{'OK'}, 0, "MAK product key could not be retrieved from the winProductKey table"); return; } # Attempt to install the MAK product key if ($self->run_slmgr_ipk($product_key)) { notify($ERRORS{'DEBUG'}, 0, "installed MAK product key: $product_key"); } else { notify($ERRORS{'CRITICAL'}, 0, "failed to install MAK product key: $product_key"); return; } # Attempt to activate the license if ($self->run_slmgr_ato()) { notify($ERRORS{'OK'}, 0, "activated Windows using MAK product key: $product_key"); return 1; } else { notify($ERRORS{'CRITICAL'}, 0, "failed to activate Windows using MAK product key: $product_key"); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 activate_kms Parameters : None Returns : If successful: true If failed: false Description : Attempts to activate Windows using a KMS server configured in the winKMS table. =cut sub activate_kms { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Get the KMS server info from the winKMS table my $kms_server_info = $self->get_kms_servers(); if (!$kms_server_info) { notify($ERRORS{'OK'}, 0, "unable to activate because the database does not contain the necessary KMS server information"); return; } # Attempt to get the KMS client product key # This is a publically available key that needs to be installed in order to activate via KMS my $product_key = $self->get_kms_client_product_key(); if ($product_key) { notify($ERRORS{'DEBUG'}, 0, "retrieved KMS client product key: $product_key"); } else { notify($ERRORS{'WARNING'}, 0, "KMS client product key could not be retrieved"); return; } # Attempt to install the KMS client product key if ($self->run_slmgr_ipk($product_key)) { notify($ERRORS{'DEBUG'}, 0, "installed KMS client product key: $product_key"); } else { notify($ERRORS{'CRITICAL'}, 0, "failed to install KMS client product key: $product_key"); return; } # Loop through the KMS servers, set KMS server, attempt to activate for my $kms_server (@{$kms_server_info}) { my $kms_address = $kms_server->{address}; my $kms_port = $kms_server->{port}; notify($ERRORS{'DEBUG'}, 0, "attempting to set KMS server: $kms_address:$kms_port"); # Run slmgr.vbs -skms to configure the computer to use the KMS server if ($self->run_slmgr_skms($kms_address, $kms_port)) { notify($ERRORS{'OK'}, 0, "set KMS server: $kms_address:$kms_port"); # Attempt to activate the license if ($self->run_slmgr_ato()) { notify($ERRORS{'OK'}, 0, "activated Windows using KMS server: $kms_address:$kms_port"); return 1; } else { notify($ERRORS{'CRITICAL'}, 0, "failed to activate Windows using KMS server: $kms_address:$kms_port"); next; } } else { notify($ERRORS{'CRITICAL'}, 0, "failed to set KMS server: $kms_address:$kms_port"); next; } } notify($ERRORS{'WARNING'}, 0, "failed to activate Windows using any KMS servers configured in the winKMS table"); return; } #////////////////////////////////////////////////////////////////////////////// =head2 run_slmgr_ipk Parameters : None Returns : If successful: true If failed: false Description : Runs slmgr.vbs -ipk to install a product key. =cut sub run_slmgr_ipk { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # Get the arguments my $product_key = shift; if (!defined($product_key) || !$product_key) { notify($ERRORS{'WARNING'}, 0, "product key was not passed correctly as an argument"); return; } # Run cscript.exe slmgr.vbs -ipk to install the product key my $ipk_command = "$system32_path/cscript.exe //NoLogo \$SYSTEMROOT/System32/slmgr.vbs -ipk $product_key"; my ($ipk_exit_status, $ipk_output) = $self->execute({ command => $ipk_command, timeout_seconds => 240, display_output => 1 }); if (defined($ipk_exit_status) && $ipk_exit_status == 0 && grep(/successfully/i, @$ipk_output)) { notify($ERRORS{'OK'}, 0, "installed product key: $product_key"); } elsif (defined($ipk_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to install product key: $product_key, exit status: $ipk_exit_status, output:\n@{$ipk_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to install product key: $product_key"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 run_slmgr_ckms Parameters : None Returns : If successful: true If failed: false Description : Runs slmgr.vbs -ckms to clear the KMS server on a Windows client. =cut sub run_slmgr_ckms { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # Run slmgr.vbs -ckms to clear an existing KMS server from a computer # slmgr.vbs must be run in a command shell using the correct System32 path or the task it's supposed to do won't really take effect my $skms_command = "$system32_path/cscript.exe //NoLogo \$SYSTEMROOT/System32/slmgr.vbs -ckms"; my ($skms_exit_status, $skms_output) = $self->execute({ command => $skms_command, timeout_seconds => 240, display_output => 1 }); if (defined($skms_exit_status) && $skms_exit_status == 0 && grep(/successfully/i, @$skms_output)) { notify($ERRORS{'OK'}, 0, "cleared kms server"); } elsif (defined($skms_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to clear kms server, exit status: $skms_exit_status, output:\n@{$skms_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to clear kms server"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 run_slmgr_cpky Parameters : None Returns : If successful: true If failed: false Description : Runs slmgr.vbs -cpky to clear the KMS server on a Windows client. =cut sub run_slmgr_cpky { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # Run slmgr.vbs -cpky to clear an existing product key from a computer # slmgr.vbs must be run in a command shell using the correct System32 path or the task it's supposed to do won't really take effect my $skms_command = "$system32_path/cscript.exe //NoLogo \$SYSTEMROOT/System32/slmgr.vbs -cpky"; my ($skms_exit_status, $skms_output) = $self->execute({ command => $skms_command, timeout_seconds => 240, display_output => 1 }); if (defined($skms_exit_status) && $skms_exit_status == 0 && grep(/successfully/i, @$skms_output)) { notify($ERRORS{'OK'}, 0, "cleared product key"); } elsif (defined($skms_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to clear product key, exit status: $skms_exit_status, output:\n@{$skms_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to clear product key"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 run_slmgr_skms Parameters : None Returns : If successful: true If failed: false Description : Runs slmgr.vbs -skms to set the KMS server on a Windows client. =cut sub run_slmgr_skms { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # Get the KMS address argument my $kms_address = shift; if (!$kms_address) { notify($ERRORS{'WARNING'}, 0, "KMS address was not passed correctly as an argument"); return; } # Get the KMS port argument or use the default port my $kms_port = shift || 1688; # Run slmgr.vbs -skms to configure the computer to use the KMS server # slmgr.vbs must be run in a command shell using the correct System32 path or the task it's supposed to do won't really take effect my $skms_command = "$system32_path/cscript.exe //NoLogo \$SYSTEMROOT/System32/slmgr.vbs -skms $kms_address:$kms_port"; my ($skms_exit_status, $skms_output) = $self->execute({ command => $skms_command, timeout_seconds => 240, display_output => 1 }); if (defined($skms_exit_status) && $skms_exit_status == 0 && grep(/successfully/i, @$skms_output)) { notify($ERRORS{'OK'}, 0, "set kms server to $kms_address:$kms_port"); } elsif (defined($skms_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to set kms server to $kms_address:$kms_port, exit status: $skms_exit_status, output:\n@{$skms_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to set kms server to $kms_address:$kms_port"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 run_slmgr_ato Parameters : None Returns : If successful: true If failed: false Description : Runs slmgr.vbs -ato to activate Windows. =cut sub run_slmgr_ato { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # Run cscript.exe slmgr.vbs -ato to install the product key my $ato_command = "$system32_path/cscript.exe //NoLogo \$SYSTEMROOT/System32/slmgr.vbs -ato"; my ($ato_exit_status, $ato_output) = $self->execute({ command => $ato_command, timeout_seconds => 240, display_output => 1 }); if (defined($ato_exit_status) && $ato_exit_status == 0 && grep(/successfully/i, @$ato_output)) { notify($ERRORS{'OK'}, 0, "activated license"); } elsif (defined($ato_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to activate license, exit status: $ato_exit_status, output:\n@{$ato_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to activate license"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 run_slmgr_dlv Parameters : None Returns : If successful: true If failed: false Description : Runs slmgr.vbs -dlv to display licensing information. =cut sub run_slmgr_dlv { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # Run cscript.exe slmgr.vbs -dlv to install the product key my $dlv_command = "$system32_path/cscript.exe //NoLogo \$SYSTEMROOT/System32/slmgr.vbs -dlv"; my ($dlv_exit_status, $dlv_output) = $self->execute({ command => $dlv_command, timeout_seconds => 120, display_output => 1 }); if (defined($dlv_exit_status) && $dlv_exit_status == 0) { notify($ERRORS{'OK'}, 0, "licensing information:\n" . join("\n", @$dlv_output)); } elsif (defined($dlv_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve licensing information, exit status: $dlv_exit_status, output:\n@{$dlv_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to retrieve licensing information"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 get_license_status Parameters : None Returns : If successful: string If failed: false Description : Runs slmgr.vbs -dlv to determine the licensing status. The value of the "License Status" line is returned. =cut sub get_license_status { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # Run cscript.exe slmgr.vbs -dlv to get the activation status my $dlv_command = "$system32_path/cscript.exe //NoLogo \$SYSTEMROOT/System32/slmgr.vbs -dlv"; my ($dlv_exit_status, $dlv_output) = $self->execute($dlv_command); if ($dlv_output && grep(/License Status/i, @$dlv_output)) { #notify($ERRORS{'DEBUG'}, 0, "retrieved license information"); } elsif (defined($dlv_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve activation status, exit status: $dlv_exit_status, output:\n@{$dlv_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to execute ssh command to retrieve activation status"); return; } my ($license_status_line) = grep(/License Status/i, @$dlv_output); my ($license_status) = $license_status_line =~ /: (.+)/; notify($ERRORS{'DEBUG'}, 0, "retrieved license status: $license_status"); return $license_status; } #////////////////////////////////////////////////////////////////////////////// =head2 deactivate Parameters : None Returns : If successful: true If failed: false Description : Deletes existing KMS servers keys from the registry. Runs cscript.exe slmgr.vbs -rearm to rearm licensing on the computer. =cut sub deactivate { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Clear the product key from the registry $self->run_slmgr_cpky(); # Clear the KMS address from the registry $self->run_slmgr_ckms(); # Set SkipRearm=1 so the rearm count isn't decremented my $registry_string .= <<'EOF'; Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SL] "SkipRearm"=dword:00000001 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SoftwareProtectionPlatform] "SkipRearm"=dword:00000001 EOF # Import the string into the registry if ($self->import_registry_string($registry_string)) { notify($ERRORS{'DEBUG'}, 0, "removed kms keys from the registry"); } else { notify($ERRORS{'WARNING'}, 0, "failed to remove kms keys from the registry"); return 0; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 set_network_location Parameters : Returns : Description : =cut sub set_network_location { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } #Category key: Home/Work=00000000, Public=00000001 my $registry_string .= <<"EOF"; Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Signatures\\FirstNetwork] "Category"=dword:00000001 EOF # Import the string into the registry if ($self->import_registry_string($registry_string)) { notify($ERRORS{'DEBUG'}, 0, "set network location"); return 1; } else { notify($ERRORS{'WARNING'}, 0, "failed to set network location"); return 0; } } #////////////////////////////////////////////////////////////////////////////// =head2 firewall_enable_ping Parameters : Returns : 1 if succeeded, 0 otherwise Description : =cut sub firewall_enable_ping { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # First delete any rules which allow ping and then add a new rule my $add_rule_command; $add_rule_command .= $system32_path . '/netsh.exe advfirewall firewall delete rule'; $add_rule_command .= ' name=all'; $add_rule_command .= ' dir=in'; $add_rule_command .= ' protocol=icmpv4:8,any'; $add_rule_command .= ' ; '; $add_rule_command .= $system32_path . '/netsh.exe advfirewall firewall add rule'; $add_rule_command .= ' name="VCL: allow ping to/from any address"'; $add_rule_command .= ' description="Allows incoming ping (ICMP type 8) messages to/from any address"'; $add_rule_command .= ' protocol=icmpv4:8,any'; $add_rule_command .= ' action=allow'; $add_rule_command .= ' enable=yes'; $add_rule_command .= ' dir=in'; $add_rule_command .= ' localip=any'; $add_rule_command .= ' remoteip=any'; # Add the firewall rule my ($add_rule_exit_status, $add_rule_output) = $self->execute($add_rule_command); if (defined($add_rule_output) && @$add_rule_output[-1] =~ /(Ok|The object already exists)/i) { notify($ERRORS{'OK'}, 0, "added firewall rule to enable ping from any address"); } elsif (defined($add_rule_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to add firewall rule to enable ping from any address, exit status: $add_rule_exit_status, output:\n@{$add_rule_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to add firewall rule to enable ping from any address"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 firewall_enable_ping_private Parameters : Returns : 1 if succeeded, 0 otherwise Description : =cut sub firewall_enable_ping_private { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # Get the computer's private IP address my $private_ip_address = $self->get_private_ip_address(); if (!$private_ip_address) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve private IP address"); return; } # First delete any rules which allow ping and then add a new rule my $add_rule_command; $add_rule_command .= $system32_path . '/netsh.exe advfirewall firewall delete rule'; $add_rule_command .= ' name=all'; $add_rule_command .= ' dir=in'; $add_rule_command .= ' protocol=icmpv4:8,any'; $add_rule_command .= ' ; '; $add_rule_command .= $system32_path . '/netsh.exe advfirewall firewall add rule'; $add_rule_command .= ' name="VCL: allow ping to ' . $private_ip_address . '"'; $add_rule_command .= ' description="Allows incoming ping (ICMP type 8) messages to ' . $private_ip_address . '"'; $add_rule_command .= ' protocol=icmpv4:8,any'; $add_rule_command .= ' action=allow'; $add_rule_command .= ' enable=yes'; $add_rule_command .= ' dir=in'; $add_rule_command .= ' localip=' . $private_ip_address; # Add the firewall rule my ($add_rule_exit_status, $add_rule_output) = $self->execute($add_rule_command); if (defined($add_rule_output) && @$add_rule_output[-1] =~ /(Ok|The object already exists)/i) { notify($ERRORS{'OK'}, 0, "added firewall rule to allow incoming ping to: $private_ip_address"); } elsif (defined($add_rule_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to add firewall rule to allow incoming ping to: $private_ip_address, exit status: $add_rule_exit_status, output:\n@{$add_rule_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to add firewall rule to allow incoming ping to: $private_ip_address"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 firewall_disable_ping Parameters : Returns : 1 if succeeded, 0 otherwise Description : =cut sub firewall_disable_ping { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # First delete any rules which allow ping and then add a new rule my $netsh_command; $netsh_command .= $system32_path . '/netsh.exe advfirewall firewall delete rule'; $netsh_command .= ' name=all'; $netsh_command .= ' dir=in'; $netsh_command .= ' protocol=icmpv4:8,any'; # Execute the netsh.exe command my ($netsh_exit_status, $netsh_output) = $self->execute($netsh_command); if (defined($netsh_output) && @$netsh_output[-1] =~ /Ok/i) { notify($ERRORS{'OK'}, 0, "configured firewall to disallow ping"); } elsif (defined($netsh_output) && @$netsh_output[-1] =~ /No rules match/i) { notify($ERRORS{'OK'}, 0, "no firewall rules exist which enable ping"); } elsif (defined($netsh_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to configure firewall to disallow ping, exit status: $netsh_exit_status, output:\n@{$netsh_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to configure firewall to disallow ping"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 firewall_enable_rdp Parameters : Remote IP address (optional) or 'private' (optional) Returns : 1 if succeeded, 0 otherwise Description : Adds Windows firewall rules to allow RDP traffic. There are 3 modes: 1. No argument is passed: RDP is allowed to/from any IP address 2. IP address argument is passed: RDP is allowed from the remote IP address specified and to the local private IP address. The argument can be a single IP address or in CIDR format. 3. The string 'private' is passed: RDP is allowed only to the local private IP address. =cut sub firewall_enable_rdp { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; my $remote_ip; my $rule_name; my $rule_description; # Check if 'private' or IP address argument was passed my $argument = shift; if ($argument) { # Check if argument is an IP address if ($argument =~ /^[\d\.\/]+$/) { $remote_ip = $argument; notify($ERRORS{'DEBUG'}, 0, "opening RDP for remote IP address: $remote_ip"); $rule_name = "VCL: allow RDP port 3389 from $remote_ip"; $rule_description = "Allows incoming TCP port 3389 traffic from $remote_ip"; } elsif ($argument eq 'private') { notify($ERRORS{'DEBUG'}, 0, "opening RDP for private IP address only"); } else { notify($ERRORS{'WARNING'}, 0, "argument may only be 'private' or an IP address in the form xxx.xxx.xxx.xxx or xxx.xxx.xxx.xxx/yy"); return; } } else { # No argument was passed, RDP will be opened to/from any address notify($ERRORS{'DEBUG'}, 0, "opening RDP to/from any IP address"); $remote_ip = 'any'; $rule_name = "VCL: allow RDP port 3389 to/from any address"; $rule_description = "Allows incoming TCP port 3389 traffic to/from any address"; } # Get the computer's private IP address my $private_ip_address = $self->get_private_ip_address(); if (!$private_ip_address) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve private IP address"); if ($argument && $argument eq 'private') { notify($ERRORS{'WARNING'}, 0, "failed to add firewall rule to enable RDP to private IP address"); return; } } my $add_rule_command; # Set the key to allow remote connections whenever enabling RDP $add_rule_command .= $system32_path . '/reg.exe ADD "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server" /t REG_DWORD /v fDenyTSConnections /d 0 /f ; '; # Set the key to allow connections from computers running any version of Remote Desktop $add_rule_command .= $system32_path . '/reg.exe ADD "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp" /t REG_DWORD /v UserAuthentication /d 0 /f ; '; # First delete any rules which allow ping and then add a new rule $add_rule_command .= "$system32_path/netsh.exe advfirewall firewall delete rule"; $add_rule_command .= " name=all"; $add_rule_command .= " dir=in"; $add_rule_command .= " protocol=TCP"; $add_rule_command .= " localport=3389"; $add_rule_command .= " ;"; # Add the rule to open RDP for the private IP address if the private IP address was found # No need to add the rule if the remote IP is any because it will be opened universally if ($private_ip_address && (!$remote_ip || ($remote_ip && $remote_ip ne 'any'))) { $add_rule_command .= " $system32_path/netsh.exe advfirewall firewall add rule"; $add_rule_command .= " name=\"VCL: allow RDP port 3389 to $private_ip_address\""; $add_rule_command .= " description=\"Allows incoming RDP (TCP port 3389) traffic to $private_ip_address\""; $add_rule_command .= " protocol=TCP"; $add_rule_command .= " localport=3389"; $add_rule_command .= " action=allow"; $add_rule_command .= " enable=yes"; $add_rule_command .= " dir=in"; $add_rule_command .= " localip=$private_ip_address"; $add_rule_command .= " ;"; } # Add the rule to open RDP for the remote public IP address if ($remote_ip) { $add_rule_command .= " $system32_path/netsh.exe advfirewall firewall add rule"; $add_rule_command .= " name=\"$rule_name\""; $add_rule_command .= " description=\"$rule_description\""; $add_rule_command .= " protocol=TCP"; $add_rule_command .= " action=allow"; $add_rule_command .= " enable=yes"; $add_rule_command .= " dir=in"; $add_rule_command .= " localip=any"; $add_rule_command .= " localport=3389"; $add_rule_command .= " remoteip=" . $remote_ip; } # Set $remote_ip for output messages if it isn't defined $remote_ip = 'private only' if !$remote_ip; # Add the firewall rule my ($add_rule_exit_status, $add_rule_output) = $self->execute($add_rule_command); if (defined($add_rule_output) && @$add_rule_output[-1] =~ /(Ok|The object already exists)/i) { notify($ERRORS{'OK'}, 0, "added firewall rule to enable RDP from $remote_ip"); } elsif (defined($add_rule_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to add firewall rule to enable RDP from $remote_ip, exit status: $add_rule_exit_status, output:\n@{$add_rule_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to add firewall rule to enable RDP from $remote_ip"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 firewall_enable_rdp_private Parameters : Returns : 1 if succeeded, 0 otherwise Description : =cut sub firewall_enable_rdp_private { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->firewall_enable_rdp('private'); } #////////////////////////////////////////////////////////////////////////////// =head2 firewall_disable_rdp Parameters : Returns : 1 if succeeded, 0 otherwise Description : =cut sub firewall_disable_rdp { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # First delete any rules which allow ping and then add a new rule my $netsh_command; $netsh_command .= $system32_path. '/netsh.exe advfirewall firewall delete rule'; $netsh_command .= ' name=all'; $netsh_command .= ' dir=in'; $netsh_command .= ' protocol=TCP'; $netsh_command .= ' localport=3389'; # Delete the firewall rule my ($netsh_exit_status, $netsh_output) = $self->execute($netsh_command); if (defined($netsh_output) && @$netsh_output[-1] =~ /(Ok|The object already exists)/i) { notify($ERRORS{'OK'}, 0, "deleted firewall rules which enable RDP"); } elsif (defined($netsh_output) && @$netsh_output[-1] =~ /No rules match/i) { notify($ERRORS{'OK'}, 0, "no firewall rules exist which enable RDP"); } elsif (defined($netsh_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to delete firewall rules which enable RDP, exit status: $netsh_exit_status, output:\n@{$netsh_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to run command to delete firewall rules which enable RDP"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 firewall_enable_ssh Parameters : Returns : 1 if succeeded, 0 otherwise Description : =cut sub firewall_enable_ssh { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } # Check if 'private' argument was passed my $enable_private = shift; if ($enable_private && $enable_private !~ /private/i) { notify($ERRORS{'WARNING'}, 0, "argument may only be the string 'private': $enable_private"); return; } my $system32_path = $self->get_system32_path() || return; my $rule_name; my $rule_description; my $rule_localip; if ($enable_private) { # Get the computer's private IP address my $private_ip_address = $self->get_private_ip_address(); if (!$private_ip_address) { notify($ERRORS{'WARNING'}, 0, "unable to retrieve private IP address"); return; } $rule_name = "VCL: allow SSH port 22 to $private_ip_address"; $rule_description = "Allows incoming SSH (TCP port 22) traffic to $private_ip_address"; $rule_localip = $private_ip_address; } else { $rule_name = "VCL: allow SSH port 22 to/from any address"; $rule_description = "Allows incoming SSH (TCP port 22) traffic to/from any address"; $rule_localip = "any"; } # Assemble a chain of commands my $add_rule_command; # Get the firewall state - "ON" or "OFF" # Turn firewall off before altering SSH exceptions or command may hang my $firewall_state = $self->get_firewall_state() || 'ON'; if ($firewall_state eq 'ON') { notify($ERRORS{'DEBUG'}, 0, "firewall is on, it will be turned off while SSH port exceptions are altered"); $add_rule_command .= $system32_path . '/netsh.exe advfirewall set currentprofile state off ; sleep 1 ; '; } # The existing matching rules must be deleted first or they will remain in effect $add_rule_command .= "$system32_path/netsh.exe advfirewall firewall delete rule"; $add_rule_command .= " name=all"; $add_rule_command .= " dir=in"; $add_rule_command .= " protocol=TCP"; $add_rule_command .= " localport=22"; $add_rule_command .= " ;"; $add_rule_command .= " $system32_path/netsh.exe advfirewall firewall add rule"; $add_rule_command .= " name=\"$rule_name\""; $add_rule_command .= " description=\"$rule_description\""; $add_rule_command .= " protocol=TCP"; $add_rule_command .= " localport=22"; $add_rule_command .= " action=allow"; $add_rule_command .= " enable=yes"; $add_rule_command .= " dir=in"; $add_rule_command .= " localip=$rule_localip"; $add_rule_command .= " remoteip=any"; # Add the firewall rule my ($add_rule_exit_status, $add_rule_output) = $self->execute($add_rule_command); if (defined($add_rule_output) && @$add_rule_output[-1] =~ /(Ok|The object already exists)/i) { notify($ERRORS{'OK'}, 0, "added firewall rule to enable SSH to address: $rule_localip"); } elsif (defined($add_rule_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to add firewall rule to enable SSH to address: $rule_localip, exit status: $add_rule_exit_status, output:\n@{$add_rule_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to add firewall rule to enable SSH to address: $rule_localip"); return; } # Turn the firewall back on after SSH exceptions are set if ($firewall_state eq 'ON') { my $firewall_enable_command = "$system32_path/netsh.exe advfirewall set currentprofile state on"; my ($firewall_enable_exit_status, $firewall_enable_output) = $self->execute($firewall_enable_command); if (defined($firewall_enable_output) && @$firewall_enable_output[-1] =~ /Ok/i) { notify($ERRORS{'OK'}, 0, "turned on firewall after turning it off to alter SSH port exceptions"); } elsif (defined($firewall_enable_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to turn on firewall after turning it off to alter SSH port exceptions, exit status: $firewall_enable_exit_status, output:\n@{$firewall_enable_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to turn on firewall after turning it off to alter SSH port exceptions"); return; } } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 firewall_enable_ssh_private Parameters : Returns : 1 if succeeded, 0 otherwise Description : =cut sub firewall_enable_ssh_private { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->firewall_enable_ssh('private'); } #////////////////////////////////////////////////////////////////////////////// =head2 get_firewall_state Parameters : None Returns : If successful: string "ON" or "OFF" Description : Determines if the Windows firewall is on or off. Returns "ON" if either the Public or Private firewall profile is on. Returns "OFF" only if all current firewall profiles are off. =cut sub get_firewall_state { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; # Run netsh.exe to get the state of the current firewall profile my $netsh_command = "$system32_path/netsh.exe advfirewall show currentprofile state"; my ($netsh_exit_status, $netsh_output) = $self->execute($netsh_command); if (defined($netsh_output)) { notify($ERRORS{'DEBUG'}, 0, "retrieved firewall state"); } elsif (defined($netsh_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve firewall state, exit status: $netsh_exit_status, output:\n@{$netsh_output}"); return; } else { notify($ERRORS{'WARNING'}, 0, "failed to retrieve firewall state"); return; } # Get the lines containing 'State' # There are multiple for the Private and Public profiles my @state_lines = grep(/State/, @$netsh_output); if (!@state_lines) { notify($ERRORS{'WARNING'}, 0, "unable to find 'State' line in output:\n" . join("\n", @$netsh_output)); return; } # Loop through lines, if any contain "ON", return "ON" for my $state_line (@state_lines) { if ($state_line =~ /on/i) { notify($ERRORS{'OK'}, 0, "returning firewall state: ON"); return "ON"; } elsif ($state_line !~ /off/i) { notify($ERRORS{'WARNING'}, 0, "firewall state line does not contain ON or OFF"); return; } } # No state lines were found containing "ON", return "OFF" notify($ERRORS{'OK'}, 0, "returning firewall state: OFF"); return "OFF"; } #////////////////////////////////////////////////////////////////////////////// =head2 get_firewall_configuration Parameters : none Returns : hash reference Description : Retrieves information about the open firewall ports on the computer and constructs a hash. The hash keys are protocol names. Each protocol key contains a hash reference. The keys are either port numbers or ICMP types. Example: "ICMP" => { 8 => { "description" => "VCL: allow ICMP/8 from 10.10.14.14", "local_ip" => "Any", "name" => "VCL: allow ICMP/8 from 10.10.14.14", "scope" => "10.10.14.14/32" } }, "TCP" => { 3389 => { "description" => "Allows incoming TCP port 3389 traffic", "local_ip" => "Any", "name" => "VCL: allow RDP port 3389", "scope" => "Any" }, }, =cut sub get_firewall_configuration { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } return $self->{firewall_configuration} if $self->{firewall_configuration}; my $computer_node_name = $self->data->get_computer_node_name(); my $system32_path = $self->get_system32_path() || return; my $firewall_configuration; my $command = "$system32_path/netsh.exe advfirewall firewall show rule name=all verbose"; my ($exit_status, $output) = $self->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to run command to show firewall rules on $computer_node_name"); return; } elsif (!grep(/Rule Name:/i, @$output)) { notify($ERRORS{'WARNING'}, 0, "unexpected output returned from command to show firewall rules on $computer_node_name, command: '$command', exit status: $exit_status, output:\n" . join("\n", @$output)); return; } # Execute the netsh.exe command to retrieve firewall rules # Rule Name: VCL: allow RDP port 3389 # ---------------------------------------------------------------------- # Enabled: Yes # Direction: In # Profiles: Domain,Private,Public # Grouping: # LocalIP: Any # RemoteIP: 152.14.53.0/26,10.10.1.2-10.10.2.22 # Protocol: TCP # LocalPort: 3389 # RemotePort: Any # Edge traversal: No # Action: Allow # Rule Name: VCL: allow ping to/from any address # ---------------------------------------------------------------------- # Enabled: Yes # Direction: In # Profiles: Domain,Private,Public # Grouping: # LocalIP: Any # RemoteIP: Any # Protocol: ICMPv4 # Type Code # 8 Any # Edge traversal: No # Action: Allow # Split the output into rule sections my @rule_sections = split(/Rule Name:\s*/, join("\n", @$output)); RULE: for my $rule_section (@rule_sections) { my @lines = split(/\n+/, $rule_section); my $rule_name = shift(@lines); # The first rule section will probably be blank because of the way split works next RULE if (!$rule_name); my $rule_info; for my $line (@lines) { if (my ($parameter, $value) = $line =~ /^(\w+):\s*(.*)/g) { $rule_info->{$parameter} = $value; } elsif ($rule_info->{Protocol} && $rule_info->{Protocol} =~ /icmp/i) { if (my ($icmp_type, $icmp_code) = $line =~ /^\s*(\d+)\s+(.*)/g) { push @{$rule_info->{ICMPTypes}{$icmp_type}}, $icmp_code; } } } if (!defined($rule_info->{Enabled}) || $rule_info->{Enabled} !~ /yes/i) { #notify($ERRORS{'DEBUG'}, 0, "ignoring disabled rule: '$rule_name'"); next RULE; } if (!defined($rule_info->{Direction}) || $rule_info->{Direction} !~ /in/i) { #notify($ERRORS{'DEBUG'}, 0, "ignoring outgoing rule: '$rule_name'"); next RULE; } elsif (!defined($rule_info->{Action}) || $rule_info->{Action} !~ /allow/i) { #notify($ERRORS{'DEBUG'}, 0, "ignoring rule: '$rule_name', Action is NOT allow"); next RULE; } elsif (!defined($rule_info->{Protocol})) { #notify($ERRORS{'DEBUG'}, 0, "ignoring rule: '$rule_name', Protocol is not defined:\n$rule_section"); next RULE; } elsif ($rule_info->{Protocol} =~ /v6/i) { # Skip IPv6 rules for now next RULE; } my @ports; if ($rule_info->{Protocol} =~ /icmp/i) { if (!defined($rule_info->{ICMPTypes})) { notify($ERRORS{'DEBUG'}, 0, "ignoring rule: '$rule_name', ICMP type could not be determined:\n$rule_section"); next RULE; } @ports = sort keys(%{$rule_info->{ICMPTypes}}) } else { if (!defined($rule_info->{LocalPort})) { #notify($ERRORS{'DEBUG'}, 0, "ignoring rule: '$rule_name', LocalPort is not defined"); next RULE; } elsif ($rule_info->{LocalPort} !~ /^\d+$/) { #notify($ERRORS{'DEBUG'}, 0, "ignoring rule: '$rule_name', LocalPort is not an integer"); next RULE; } @ports = split(",", $rule_info->{LocalPort}); } if (!@ports) { notify($ERRORS{'WARNING'}, 0, "ignoring rule: '$rule_name', no ports defined:\n" . format_data($rule_info) . "\n$rule_section"); next RULE; } for my $port (@ports) { $firewall_configuration->{$rule_info->{Protocol}}{$port}{name} = $rule_name; $firewall_configuration->{$rule_info->{Protocol}}{$port}{description} = $rule_info->{Description}; $firewall_configuration->{$rule_info->{Protocol}}{$port}{scope} = $rule_info->{RemoteIP}; $firewall_configuration->{$rule_info->{Protocol}}{$port}{local_ip} = $rule_info->{LocalIP}; } } # Assemble a string containing all the rule info (don't print_data because it outputs too much to vcld.log) my $rules_string; for my $protocol (keys %$firewall_configuration) { for my $port (sort keys %{$firewall_configuration->{$protocol}}) { my $name = $firewall_configuration->{$protocol}{$port}{name}; my $scope = $firewall_configuration->{$protocol}{$port}{scope}; my $local_ip = $firewall_configuration->{$protocol}{$port}{local_ip}; $rules_string .= "$protocol:$port '$name' - local IP: $local_ip, scope: $scope\n"; } } # Copy the ICMPv4 key to one named ICMP for compatibility if (defined($firewall_configuration->{ICMPv4})) { $firewall_configuration->{ICMP} = $firewall_configuration->{ICMPv4}; } $self->{firewall_configuration} = $firewall_configuration; notify($ERRORS{'DEBUG'}, 0, "retrieved firewall info from $computer_node_name:\n$rules_string"); return $firewall_configuration; } #////////////////////////////////////////////////////////////////////////////// =head2 _enable_firewall_port_helper Parameters : Returns : boolean Description : This subroutine is called by enable_firewall_port. It runs the necessary 'netsh advfirewall' command to configure the firewall. =cut sub _enable_firewall_port_helper { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my ($protocol, $port, $scope, $overwrite_existing, $name, $description) = @_; if (!defined($protocol) || !defined($port) || !defined($scope) || !defined($name)) { notify($ERRORS{'WARNING'}, 0, "protocol, port, scope, and name arguments were not supplied"); return; } my $computer_node_name = $self->data->get_computer_node_name(); my $system32_path = $self->get_system32_path() || return; $scope = 'any' if $scope eq '0.0.0.0/0.0.0.0'; my $netsh_command; if ($protocol =~ /icmp/i) { $netsh_command .= "$system32_path/netsh.exe advfirewall firewall delete rule"; $netsh_command .= " name=all"; $netsh_command .= " dir=in"; $netsh_command .= " protocol=icmpv4:$port,any"; $netsh_command .= " ; "; $netsh_command .= " $system32_path/netsh.exe advfirewall firewall add rule"; $netsh_command .= " name=\"$name\""; $netsh_command .= " description=\"$description\""; $netsh_command .= " protocol=icmpv4:$port,any"; $netsh_command .= " action=allow"; $netsh_command .= " enable=yes"; $netsh_command .= " dir=in"; $netsh_command .= " localip=any"; $netsh_command .= " remoteip=$scope"; } else { $netsh_command .= "$system32_path/netsh.exe advfirewall firewall delete rule"; $netsh_command .= " name=all"; $netsh_command .= " dir=in"; $netsh_command .= " protocol=$protocol"; $netsh_command .= " localport=$port"; $netsh_command .= " ;"; $netsh_command .= " $system32_path/netsh.exe advfirewall firewall add rule"; $netsh_command .= " name=\"$name\""; $netsh_command .= " description=\"$description\""; $netsh_command .= " protocol=$protocol"; $netsh_command .= " action=allow"; $netsh_command .= " enable=yes"; $netsh_command .= " dir=in"; $netsh_command .= " localip=any"; $netsh_command .= " localport=$port"; $netsh_command .= " remoteip=$scope"; } # Execute the netsh.exe command my ($netsh_exit_status, $netsh_output) = $self->execute($netsh_command, 1); if (!defined($netsh_output)) { notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to open firewall on $computer_node_name, command: '$netsh_command'"); return; } elsif (@$netsh_output[-1] =~ /(Ok|The object already exists)/i) { notify($ERRORS{'OK'}, 0, "opened firewall on $computer_node_name:\n" . "name: '$name'\n" . "protocol: $protocol\n" . "port/type: $port\n" . "scope: $scope" ); return 1; } else { notify($ERRORS{'WARNING'}, 0, "failed to open firewall on $computer_node_name:\n" . "name: '$name'\n" . "protocol: $protocol\n" . "port/type: $port\n" . "scope: $scope\n" . "command : '$netsh_command'" . "exit status: $netsh_exit_status\n" . "output:\n" . join("\n", @$netsh_output) ); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 run_sysprep Parameters : None Returns : 1 if successful, 0 otherwise Description : =cut sub run_sysprep { my $self = shift; if (ref($self) !~ /windows/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $computer_node_name = $self->data->get_computer_node_name(); my $system32_path = $self->get_system32_path() || return; my $node_configuration_directory = $self->get_node_configuration_directory(); my $time_zone_name = $self->get_time_zone_name(); if (!$time_zone_name) { notify($ERRORS{'WARNING'}, 0, "time zone name could not be retrieved"); return; } my $product_key = $self->get_kms_client_product_key(); if (!$product_key) { notify($ERRORS{'WARNING'}, 0, "KMS client product key could not be retrieved"); return; } # Set the processorArchitecture to either amd64 or x86 in the XML depending on whether or not the OS is 64-bit my $architecture = $self->is_64_bit() ? 'amd64' : 'x86'; my $unattend_xml_file_path = "$system32_path/sysprep/Unattend.xml"; my $unattend_xml_contents = <<EOF; <?xml version="1.0" encoding="utf-8"?> <unattend xmlns="urn:schemas-microsoft-com:unattend"> <settings pass="generalize"> <component name="Microsoft-Windows-PnpSysprep" processorArchitecture="$architecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <PersistAllDeviceInstalls>false</PersistAllDeviceInstalls> <DoNotCleanUpNonPresentDevices>false</DoNotCleanUpNonPresentDevices> </component> <component name="Microsoft-Windows-Security-SPP" processorArchitecture="$architecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SkipRearm>1</SkipRearm> </component> </settings> <settings pass="specialize"> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="$architecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Display> <ColorDepth>32</ColorDepth> <DPI>120</DPI> <HorizontalResolution>1024</HorizontalResolution> <VerticalResolution>768</VerticalResolution> <RefreshRate>72</RefreshRate> </Display> <ComputerName>*</ComputerName> <TimeZone>$time_zone_name</TimeZone> <ProductKey>$product_key</ProductKey> </component> <component name="Microsoft-Windows-Deployment" processorArchitecture="$architecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <RunSynchronous> <RunSynchronousCommand wcm:action="add"> <Path>C:\\Cygwin\\home\\root\\VCL\\Scripts\\sysprep_cmdlines.cmd &gt; C:\\cygwin\\home\\root\\VCL\\Logs\\sysprep_cmdlines.log 2&gt;&amp;1</Path> <Order>1</Order> </RunSynchronousCommand> </RunSynchronous> </component> <component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="$architecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SkipAutoActivation>true</SkipAutoActivation> </component> <component name="Microsoft-Windows-UnattendedJoin" processorArchitecture="$architecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Identification> <JoinWorkgroup>VCL</JoinWorkgroup> </Identification> </component> </settings> <settings pass="auditSystem"> <component name="Microsoft-Windows-PnpCustomizationsNonWinPE" processorArchitecture="$architecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <DriverPaths> <PathAndCredentials wcm:action="add" wcm:keyValue="1"> <Path>C:\\Cygwin\\home\\root\\VCL\\Drivers</Path> </PathAndCredentials> </DriverPaths> </component> </settings> <settings pass="oobeSystem"> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="$architecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <OOBE> <HideEULAPage>true</HideEULAPage> <NetworkLocation>Work</NetworkLocation> <ProtectYourPC>3</ProtectYourPC> </OOBE> <UserAccounts> <AdministratorPassword> <Value>$WINDOWS_ROOT_PASSWORD</Value> <PlainText>true</PlainText> </AdministratorPassword> <LocalAccounts> <LocalAccount wcm:action="add"> <Password> <Value>$WINDOWS_ROOT_PASSWORD</Value> <PlainText>true</PlainText> </Password> <Group>Administrators</Group> <Name>root</Name> <DisplayName>root</DisplayName> <Description>VCL root account</Description> </LocalAccount> </LocalAccounts> </UserAccounts> </component> <component name="Microsoft-Windows-International-Core" processorArchitecture="$architecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <InputLocale>en-US</InputLocale> <SystemLocale>en-US</SystemLocale> <UILanguage>en-US</UILanguage> <UserLocale>en-US</UserLocale> </component> </settings> </unattend> EOF notify($ERRORS{'DEBUG'}, 0, "'$unattend_xml_file_path' contents:\n$unattend_xml_contents"); if (!$self->create_text_file($unattend_xml_file_path, $unattend_xml_contents)) { return; } # Delete existing Panther directory, contains Sysprep log files $self->delete_file('C:/Windows/Panther'); # Delete existing sysprep/Panther directory, contains Sysprep log files $self->delete_file("$system32_path/sysprep/Panther"); # Delete existing setupapi files $self->delete_file('C:/Windows/inf/setupapi*'); # Delete existing INFCACHE files $self->delete_file('C:/Windows/inf/INFCACHE*'); # Delete existing INFCACHE files $self->delete_file('C:/Windows/inf/oem*.inf'); # Delete existing Sysprep_succeeded.tag file $self->delete_file("$system32_path/sysprep/Sysprep*.tag"); # Delete existing MSDTC.LOG file $self->delete_file("$system32_path/MsDtc/MSTTC.LOG"); # Delete existing VCL log files $self->delete_file("C:/Cygwin/home/root/VCL/Logs/*"); # Delete legacy Sysprep directory $self->delete_file("C:/Cygwin/home/root/VCL/Utilities/Sysprep"); # Grant permissions to the SYSTEM user - this is needed or else Sysprep fails $self->execute("cmd.exe /c \"$system32_path/icacls.exe $node_configuration_directory /grant SYSTEM:(OI)(CI)(F) /C\""); # Uninstall and reinstall MsDTC my $msdtc_command = "$system32_path/msdtc.exe -uninstall ; $system32_path/msdtc.exe -install"; my ($msdtc_status, $msdtc_output) = $self->execute($msdtc_command); if (defined($msdtc_status) && $msdtc_status == 0) { notify($ERRORS{'DEBUG'}, 0, "reinstalled MsDtc"); } elsif (defined($msdtc_status)) { notify($ERRORS{'WARNING'}, 0, "failed to reinstall MsDtc, exit status: $msdtc_status, output:\n@{$msdtc_output}"); } else { notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to reinstall MsDtc"); } # Get the node drivers directory and convert it to DOS format my $drivers_directory = "$node_configuration_directory/Drivers"; $drivers_directory =~ s/\//\\\\/g; # Set the Installation Sources registry key # Must use reg_add because the type is REG_MULTI_SZ my $setup_key = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup'; if ($self->reg_add($setup_key, 'Installation Sources', 'REG_MULTI_SZ', $drivers_directory)) { notify($ERRORS{'DEBUG'}, 0, "added Installation Sources registry key"); } else { notify($ERRORS{'WARNING'}, 0, "failed to add Installation Sources registry key"); } # Set the DevicePath registry key # This is used to locate device drivers if (!$self->set_device_path_key()) { notify($ERRORS{'WARNING'}, 0, "failed to set the DevicePath registry key"); return; } # Reset the Windows setup registry keys # If Sysprep fails it will set keys which make running Sysprep again impossible # These keys never get reset, Microsoft instructs you to reinstall the OS # Clearing out these keys before running Sysprep allows it to be run again # Also enable verbose Sysprep logging my $registry_string .= <<"EOF"; Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup] "LogLevel"=dword:0000FFFF [HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State] "ImageState"="IMAGE_STATE_COMPLETE" [HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\Sysprep\\Generalize] "{82468857-ad9b-1a37-533f-7db889fff253}"=- [-HKEY_LOCAL_MACHINE\\SYSTEM\\Setup\\Status] EOF # Import the string into the registry if ($self->import_registry_string($registry_string)) { notify($ERRORS{'OK'}, 0, "reset Windows setup state in the registry"); } else { notify($ERRORS{'WARNING'}, 0, "failed to reset the Windows setup state in the registry"); return 0; } # Kill the screen saver process, it occasionally prevents reboots and shutdowns from working $self->kill_process('logon.scr'); # Run Sysprep.exe, use cygstart to lauch the .exe and return immediately my $sysprep_command = "/bin/cygstart.exe \$SYSTEMROOT/system32/cmd.exe /c \""; # Run Sysprep.exe $sysprep_command .= "$system32_path/sysprep/sysprep.exe /generalize /oobe /shutdown /quiet /unattend:\$SYSTEMROOT/System32/sysprep/Unattend.xml"; $sysprep_command .= "\""; # Run Sysprep.exe, use cygstart to lauch the .exe and return immediately my ($sysprep_status, $sysprep_output) = $self->execute($sysprep_command); if (defined($sysprep_status) && $sysprep_status == 0) { notify($ERRORS{'OK'}, 0, "initiated Sysprep.exe, waiting for $computer_node_name to become unresponsive"); } elsif (defined($sysprep_status)) { notify($ERRORS{'OK'}, 0, "failed to initiate Sysprep.exe, exit status: $sysprep_status, output:\n@{$sysprep_output}"); return 0; } else { notify($ERRORS{'WARNING'}, 0, "unable to run ssh command to initiate Sysprep.exe"); return 0; } # Wait maximum of 30 minutes for the computer to become unresponsive if (!$self->wait_for_no_ping(1800)) { # Computer never stopped responding to ping notify($ERRORS{'WARNING'}, 0, "$computer_node_name never became unresponsive to ping"); return; } # Wait maximum of 15 minutes for computer to power off my $power_off = $self->provisioner->wait_for_power_off(900); if (!defined($power_off)) { # wait_for_power_off result will be undefined if the provisioning module doesn't implement a power_status subroutine notify($ERRORS{'OK'}, 0, "unable to determine power status of $computer_node_name from provisioning module, sleeping 5 minutes to allow computer time to shutdown"); sleep 300; } elsif (!$power_off) { notify($ERRORS{'WARNING'}, 0, "$computer_node_name never powered off"); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 set_ignore_default_routes Parameters : Interface type (public or private), mode (enabled or disabled) Returns : If successful: true If failed: false Description : Configures the public interface with "ignore default routes = disabled" and the private interface with "ignore default routes = enabled". This is necessary in order for traffic to be correctly routed out of the computer. If default routes are configured for both the public and private interfaces and the metric for the private default route is equal to or less than the metric for the public route, traffic originating from the computer to the Internet will fail because it will be routed on the private interface. =cut sub set_ignore_default_routes { my $self = shift; unless (ref($self) && $self->isa('VCL::Module')) { notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a VCL::Module module object method"); return; } my $system32_path = $self->get_system32_path() || return; # Get the private interface name my $private_interface_name = $self->get_private_interface_name(); if (!$private_interface_name) { notify($ERRORS{'WARNING'}, 0, "unable to determine private interface name"); return; } # Get the public interface name my $public_interface_name = $self->get_public_interface_name(); if (!$public_interface_name) { notify($ERRORS{'WARNING'}, 0, "unable to determine public interface name"); return; } # Run netsh.exe to configure any default routes configured for the public interface to be used my $netsh_command = "$system32_path/netsh.exe interface ip set interface \"$public_interface_name\" ignoredefaultroutes=disabled"; # If multiple interfaces are used, set the private interface to ignore default routes if ($private_interface_name ne $public_interface_name) { notify($ERRORS{'DEBUG'}, 0, "computer has multiple network interfaces, configuring ignore default routes:\nprivate interface '$private_interface_name': enabled\npublic interface '$public_interface_name': disabled"); $netsh_command .= " & $system32_path/netsh.exe interface ip set interface \"$private_interface_name\" ignoredefaultroutes=enabled"; } else { notify($ERRORS{'DEBUG'}, 0, "computer has a single network interface, configuring ignore default routes:\ninterface '$private_interface_name': enabled"); } my ($netsh_exit_status, $netsh_output) = $self->execute($netsh_command); if (!defined($netsh_output)) { notify($ERRORS{'WARNING'}, 0, "failed to run ssh command to configure ignore default routes"); return; } elsif ($netsh_exit_status == 0) { notify($ERRORS{'OK'}, 0, "configured ignore default routes"); } elsif (defined($netsh_exit_status)) { notify($ERRORS{'WARNING'}, 0, "failed to configure ignore default routes, exit status: $netsh_exit_status\ncommand: '$netsh_command'\noutput:\n" . join("\n", @$netsh_output)); return; } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 defragment_hard_drive Parameters : None Returns : 1 Description : Hard drive defragmentation is skipped for Windows version 6.x (Vista and Server 2008) because it takes a very long time. This subroutine always returns 1. =cut sub defragment_hard_drive { # Skip hard drive defragmentation because it takes a very long time for Windows 6.x (Vista, 2008) notify($ERRORS{'OK'}, 0, "skipping hard drive defragmentation for Windows 6.x because it takes too long, returning 1"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 wait_for_response Parameters : None Returns : If successful: true If failed: false Description : Waits for the reservation computer to respond to SSH after it has been loaded. =cut sub wait_for_response { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $initial_delay_seconds; my $ssh_response_timeout_seconds; if ($self->data->get_computer_type() eq 'virtualmachine') { $initial_delay_seconds = 5; $ssh_response_timeout_seconds = 300; } elsif ($self->data->get_imagemeta_sysprep()) { $initial_delay_seconds = 60; $ssh_response_timeout_seconds = 1800; } else { $initial_delay_seconds = 15; $ssh_response_timeout_seconds = 600; } if ($self->SUPER::wait_for_response($initial_delay_seconds, $ssh_response_timeout_seconds, 5)) { return 1; } if ($self->provisioner->can('power_reset')) { if ($self->provisioner->power_reset()) { return $self->SUPER::wait_for_response(15, 600, 5); } else { notify($ERRORS{'WARNING'}, 0, "computer never responded and provisioning module failed to perform a power reset, returning false"); return; } } else { notify($ERRORS{'WARNING'}, 0, "computer never responded and provisioning does not implement a power_reset subroutine, returning false"); return; } } #////////////////////////////////////////////////////////////////////////////// =head2 sanitize_files Parameters : none Returns : boolean Description : Removes the Windows root password from files on the computer. =cut sub sanitize_files { my $self = shift; unless (ref($self) && $self->isa('VCL::Module')) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $system32_path = $self->get_system32_path() || return; my @file_paths = ( "$system32_path/sysprep", '$SYSTEMROOT/Panther', ); # Call the subroutine in Windows.pm return $self->SUPER::sanitize_files(@file_paths); } #////////////////////////////////////////////////////////////////////////////// =head2 disable_sleep Parameters : None Returns : If successful: true If failed: false Description : Disables the sleep power mode. =cut sub disable_sleep { my $self = shift; unless (ref($self) && $self->isa('VCL::Module')) { notify($ERRORS{'CRITICAL'}, 0, "subroutine can only be called as a VCL::Module module object method"); return; } my $system32_path = $self->get_system32_path() || return; # Run powercfg.exe to disable sleep my $powercfg_command; $powercfg_command .= "$system32_path/powercfg.exe -CHANGE -monitor-timeout-ac 0 ; "; $powercfg_command .= "$system32_path/powercfg.exe -CHANGE -monitor-timeout-dc 0 ; "; $powercfg_command .= "$system32_path/powercfg.exe -CHANGE -disk-timeout-ac 0 ; "; $powercfg_command .= "$system32_path/powercfg.exe -CHANGE -disk-timeout-dc 0 ; "; $powercfg_command .= "$system32_path/powercfg.exe -CHANGE -standby-timeout-ac 0 ; "; $powercfg_command .= "$system32_path/powercfg.exe -CHANGE -standby-timeout-dc 0 ; "; $powercfg_command .= "$system32_path/powercfg.exe -CHANGE -hibernate-timeout-ac 0 ; "; $powercfg_command .= "$system32_path/powercfg.exe -CHANGE -hibernate-timeout-dc 0"; my ($powercfg_exit_status, $powercfg_output) = $self->execute($powercfg_command); if (!defined($powercfg_output)) { notify($ERRORS{'WARNING'}, 0, "failed to run SSH command to disable sleep"); return; } elsif (grep(/(error|invalid|not found)/i, @$powercfg_output)) { notify($ERRORS{'WARNING'}, 0, "failed to disable sleep, powercfg.exe output:\n" . join("\n", @$powercfg_output)); return; } else { notify($ERRORS{'OK'}, 0, "disabled sleep"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 query_event_log Parameters : $event_log_name, $xpath_query, $event_count_limit (optional), $display_output (optional) Returns : array Description : Queries the event log on the computer. The $event_log_name argument refers to the 'Channel' property of the events to be queried. Examples: Security Microsoft-Windows-GroupPolicy/Operational The $xpath_query argument is an XPath query filter. Examples: * *[System[Provider[@Name="Microsoft-Windows-Security-Auditing"] and Task=12544 and EventID=4624] and EventData[Data[@Name="LogonType"]="10"]] *[System[TimeCreated[timediff(@SystemTime) <= $milliseconds]]] *[System[Provider[@Name="Microsoft-Windows-Time-Service"]]] Constructs an array of hashes based on the XML output from wevtutil.exe. =cut sub query_event_log { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my ($event_log_name, $xpath_query, $event_count_limit, $display_output) = @_; if (!$event_log_name) { notify($ERRORS{'WARNING'}, 0, "event log name argument was not specified"); return; } $xpath_query = '*' if !$xpath_query; my $computer_node_name = $self->data->get_computer_node_name(); my $system32_path = $self->get_system32_path() || return; # Fix problems with the query - replace all single quotes with double quotes # Escape all double quote characters $xpath_query =~ s/\\?("|')/\\"/g; # Remove newlines $xpath_query =~ s/\s*\n+\s*/ /g; # Remove spaces after opening brackets and before closing brackets $xpath_query =~ s/(\[)\s+/$1/g; $xpath_query =~ s/\s+(\])/$1/g; # Remove spaces from end $xpath_query =~ s/\s+$//g; # Write the output to a file on my $command = "$system32_path/wevtutil.exe query-events $event_log_name /format:XML /element:Events /query:\"$xpath_query\""; if ($event_count_limit) { $command .= " /count:$event_count_limit"; } my ($exit_status, $output) = $self->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to query event log on $computer_node_name: $command"); return; } elsif (!grep(/<Events>/, @$output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute query event log on $computer_node_name, output does not contain expected '<Events>' text\ncommand: $command\noutput:\n" . join("\n", @$output)); return; } # Convert the XML output to a hash my $xml_hash = xml_string_to_hash($output, ['Event', 'Data']); if (!$xml_hash) { notify($ERRORS{'WARNING'}, 0, "failed to query event log on $computer_node_name, XML output could not be converted to a hash:\n" . join("\n", @$output)); return; } # If no events were returned 'Event' key will not be defined if (!defined($xml_hash->{Event})) { notify($ERRORS{'DEBUG'}, 0, "no events exist in '$event_log_name' event log on $computer_node_name, command:\n$command"); return {}; } $xml_hash = $self->query_event_log_helper($xml_hash); # Get the array of events my @events = @{$xml_hash->{Event}}; my $event_count = scalar(@events); my $levels = { 0 => 'Undefined', 1 => 'Critical', 2 => 'Error', 3 => 'Warning', 4 => 'Information', 5 => 'Verbose', }; for my $event (@events) { my $level = $event->{System}{Level}; if (defined($level) && $levels->{$level}) { $event->{System}{LEVEL_NAME} = $levels->{$level}; } else { $event->{System}{LEVEL_NAME} = '<unknown>'; } } notify($ERRORS{'DEBUG'}, 0, "retrieved $event_count $event_log_name event" . ($event_count == 1 ? '' : 's') . " from $computer_node_name, command:\n$command\n" . format_data(\@events)) if $display_output; return @events; } #////////////////////////////////////////////////////////////////////////////// =head2 query_event_log_helper Parameters : $data Returns : varies Description : Cleans up the data structure containing the event log information. If the data contains an array of hashes and each hash only has a 'Name' and 'content' key, the array is replaced with a hash whose keys are the 'Name' values and values are the 'content' values. =cut sub query_event_log_helper { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $data = shift; if (!defined($data)) { return; } my $type = ref($data); if (!$type) { return $data; } elsif ($type eq 'HASH') { for my $key (keys %$data) { $data->{$key} = $self->query_event_log_helper($data->{$key}); } return $data; } elsif ($type eq 'ARRAY') { my $test_element = @{$data}[0]; my $test_element_type = ref($test_element); if ($test_element_type && $test_element_type eq 'HASH' && defined($test_element->{Name}) && defined($test_element->{content})) { my %hash; for my $element (@$data) { if (defined($element->{Name}) && defined($element->{content})) { $hash{$element->{Name}} = $element->{content}; } } return \%hash; } my @array; for my $element (@$data) { push @array, $self->query_event_log_helper($element); } return \@array; } else { return; } } #////////////////////////////////////////////////////////////////////////////// =head2 get_time_service_events Parameters : none Returns : array Description : Retrieves event log entries generated by the Windows Time (w32time) service. Returns an array of hash references: [ { "EventData" => { "Data" => { "TimeSource" => "pool.ntp.org (ntp.m|0x0|0.0.0.0:123-><IP Address>:123)" }, "Name" => "TMP_EVENT_TIME_SOURCE_REACHABLE" }, "System" => { "Channel" => "System", "Computer" => "win2012r2", "Correlation" => {}, "EventID" => 37, "EventRecordID" => 3242, "Execution" => { "ProcessID" => 796, "ThreadID" => 1204 }, "Keywords" => "0x8000000000000000", "Level" => 4, "Opcode" => 0, "Provider" => { "Guid" => "{06EDCFEB-0FD0-4E53-ACCA-A6F8BBF81BCB}", "Name" => "Microsoft-Windows-Time-Service" }, "Security" => { "UserID" => "S-1-5-19" }, "Task" => 0, "TimeCreated" => { "SystemTime" => "2017-07-28T12:08:12.195010600Z" }, "Version" => 0 }, "xmlns" => "http://schemas.microsoft.com/win/2004/08/events/event" }, { ... }, ] =cut sub get_time_service_events { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my $computer_name = $self->data->get_computer_short_name(); my @events = $self->query_event_log('System', '*[System[Provider[@Name="Microsoft-Windows-Time-Service"]]]', 100, 0); my $info_string; for my $event (@events) { my $level = $event->{System}{LEVEL_NAME}; my $system_time = $event->{System}{TimeCreated}{SystemTime}; my $event_name = $event->{EventData}{Name}; my $event_data = $event->{EventData}{Data}; $info_string .= "($level) $system_time - $event_name\n" . format_data($event_data) . "\n\n"; } notify($ERRORS{'DEBUG'}, 0, "retrieved time service entries from event log on $computer_name:\n$info_string"); return @events; } #////////////////////////////////////////////////////////////////////////////// =head2 get_logon_events Parameters : $past_minutes (optional) Returns : array Description : Queries the event log for logon events in either the Security log or Microsoft-Windows-TerminalServices-LocalSessionManager. Anonymous logon and service logons are ignored. An array is returned sorted by time from oldest to newest. Example: [ { "datetime" => "2014-03-18 19:15:25", "description" => "An account was successfully logged on", "epoch" => "1395184525", "event_id" => 4624, "event_record_id" => 2370, "logon_type" => "Interactive", "logon_type_id" => 2, "pid" => 4624, "provider" => "Microsoft-Windows-Security-Auditing", "remote_ip" => "127.0.0.1", "user" => "root" }, { "datetime" => "2014-03-19 17:06:37", "description" => "An account was successfully logged on", "epoch" => "1395263197", "event_id" => 4624, "event_record_id" => 2665, "logon_type" => "Network", "logon_type_id" => 3, "pid" => 4624, "provider" => "Microsoft-Windows-Security-Auditing", "user" => "Administrator" }, ] =cut sub get_logon_events { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return; } my ($past_minutes) = @_; my $offset_minutes = $self->get_timezone_offset_minutes() || 0; my $logon_type_names = { 0 => 'System', 2 => 'Interactive', 3 => 'Network', 4 => 'Batch', 5 => 'Service', 6 => 'Proxy', 7 => 'Unlock', 8 => 'NetworkCleartext', 9 => 'NewCredentials', 10 => 'RemoteInteractive', 11 => 'CachedInteractive', 12 => 'CachedRemoteInteractive', 13 => 'CachedUnlock', }; my $security_event_log = 'Security'; my $lsm_event_log = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'; my $event_ids = { 'Microsoft-Windows-Security-Auditing' => { 4624 => 'An account was successfully logged on', }, 'Microsoft-Windows-TerminalServices-LocalSessionManager' => { 21 => 'Remote Desktop Services: Session logon succeeded', 25 => 'Remote Desktop Services: Session reconnection succeeded', 1101 => 'Remote Desktop Services: Session logon succeeded', 1105 => 'Remote Desktop Services: Session reconnection succeeded', }, }; my $security_event_id_string = "EventID=" . join(' or EventID=', keys(%{$event_ids->{'Microsoft-Windows-Security-Auditing'}})); my $lsm_event_id_string = "EventID=" . join(' or EventID=', keys(%{$event_ids->{'Microsoft-Windows-TerminalServices-LocalSessionManager'}})); my $time_created_string = ''; if ($past_minutes) { my $milliseconds = ($past_minutes * 60 * 1000); $time_created_string = "and TimeCreated[timediff(\@SystemTime) <= $milliseconds]"; } my $security_query = <<EOF; *[ System[ Provider[\@Name="Microsoft-Windows-Security-Auditing"] and Task=12544 and ($security_event_id_string) $time_created_string ] and EventData[ Data[\@Name="TargetUserName"]!="ANONYMOUS LOGON" and Data[\@Name="TargetUserName"]!="SYSTEM" and Data[\@Name="IpAddress"]!="127.0.0.1" and Data[\@Name="LogonType"]!="5" ] ] EOF my $lsm_query = <<EOF; *[ System[ Provider[\@Name="Microsoft-Windows-TerminalServices-LocalSessionManager"] and ($lsm_event_id_string) $time_created_string ] ] EOF my (@security_events, @lsm_events); @security_events = $self->query_event_log($security_event_log, $security_query); @lsm_events = $self->query_event_log($lsm_event_log, $lsm_query); my $logon_event_hash = {}; for my $event (@security_events, @lsm_events) { my $system = $event->{System} || next; my $provider_name = $system->{Provider}{Name}; my $system_time = $system->{TimeCreated}{SystemTime}; my $event_record_id = $system->{EventRecordID}; my $event_id = $system->{EventID}; my $process_pid = $system->{Execution}{ProcessID}; my $logon_event = { event_record_id => $event_record_id, event_id => $event_id, provider => $provider_name, pid => $event_id, }; $logon_event->{description} = $event_ids->{$provider_name}{$event_id} if $event_ids->{$provider_name}{$event_id}; # Convert system time format to datetime: 2014-03-18T19:18:41.421250000Z my ($date, $time) = $system_time =~ /^([\d-]+)T([\d:]+)\./; next if (!$date || !$time); my $datetime = "$date $time"; # The time returned is UTC and not adjusted for the computer's time zone my $epoch_seconds = convert_to_epoch_seconds($datetime); $epoch_seconds += ($offset_minutes * 60); $datetime = convert_to_datetime($epoch_seconds); $logon_event->{datetime} = $datetime; $logon_event->{epoch} = $epoch_seconds; if ($provider_name eq 'Microsoft-Windows-Security-Auditing') { my $event_data = $event->{EventData}{Data} || next; $logon_event->{user} = $event_data->{TargetUserName}; $logon_event->{remote_ip} = $event_data->{IpAddress} if $event_data->{IpAddress}; $logon_event->{remote_port} = $event_data->{IpPort} if $event_data->{IpPort}; $logon_event->{logon_type_id} = $event_data->{LogonType} if $event_data->{LogonType}; my $logon_type_id = $event_data->{LogonType}; if (defined($logon_type_id)) { $logon_event->{logon_type_id} = $logon_type_id; $logon_event->{logon_type} = $logon_type_names->{$logon_type_id} if $logon_type_names->{$logon_type_id}; } } elsif ($provider_name eq 'Microsoft-Windows-TerminalServices-LocalSessionManager') { my $user_data = $event->{UserData}{EventXML} || next;; $logon_event->{user} = $user_data->{User}; $logon_event->{remote_ip} = $user_data->{Address} if $user_data->{Address}; $logon_event->{session_id} = $user_data->{SessionID} if $user_data->{SessionID}; } if (!$logon_event->{user} || ref($logon_event->{user})) { next; } $logon_event->{user} =~ s/.*\\+//g; if ($logon_event->{remote_ip} && ($logon_event->{remote_ip} eq '0.0.0.0' || $logon_event->{remote_ip} =~ /-/)) { delete $logon_event->{remote_ip}; } if ($logon_event->{remote_port} && $logon_event->{remote_port} =~ /-/) { delete $logon_event->{remote_port}; } # Add to the hash - use key containing the provider name and record ID in case events have the same epoch time $logon_event_hash->{"$epoch_seconds-$provider_name-$event_record_id"} = $logon_event; } # Convert the hash to an array sorted by the epoch time keys my @logon_events = map { $logon_event_hash->{$_} } sort keys %$logon_event_hash; my $logon_event_count = scalar(@logon_events); notify($ERRORS{'DEBUG'}, 0, "retrieved $logon_event_count logon event" . ($logon_event_count == 1 ? '' : 's') . ":\n" . format_data(\@logon_events)); return @logon_events; } #////////////////////////////////////////////////////////////////////////////// 1; __END__ =head1 SEE ALSO L<http://cwiki.apache.org/VCL/> =cut