blob: 90b999189c7878e4ea4713d36d28ec27f37c7114 [file] [log] [blame]
#!/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';
# 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