shell bypass 403
package FusionInventory::Agent::Tools::Linux;
use strict;
use warnings;
use parent 'Exporter';
# Constant for ethtool system call
use constant SIOCETHTOOL => 0x8946 ; # See linux/sockios.h
use constant ETHTOOL_GSET => 0x00000001 ; # See linux/ethtool.h
use constant SPEED_UNKNOWN => 65535 ; # See linux/ethtool.h, to be read as -1
use English qw(-no_match_vars);
use File::Basename qw(basename dirname);
use Memoize;
use Socket qw(PF_INET SOCK_DGRAM);
use FusionInventory::Agent::Tools;
use FusionInventory::Agent::Tools::Unix;
use FusionInventory::Agent::Tools::Network;
our @EXPORT = qw(
getDevicesFromUdev
getDevicesFromHal
getDevicesFromProc
getCPUsFromProc
getInfoFromSmartctl
getInterfacesFromIfconfig
getInterfacesFromIp
getInterfacesInfosFromIoctl
);
memoize('getDevicesFromUdev');
sub getDevicesFromUdev {
my (%params) = @_;
my @devices;
# We need to support dump params to permit full testing when root params is set
my $root = $params{root} || "";
foreach my $file (glob "$root/dev/.udev/db/*") {
if ($params{dump} && -e $file) {
my $base = basename($file);
my $content = getAllLines(file => $file);
$params{dump}->{dev}->{'.udev'}->{db}->{$base} = $content;
}
my $device = getFirstMatch(
file => $file,
pattern => qr/^N:(\S+)/
);
next unless $device;
next unless $device =~ /([hsv]d[a-z]+|sr\d+)$/;
my $parsed = _parseUdevEntry(
logger => $params{logger},
file => $file,
device => $device
);
push @devices, $parsed if $parsed;
}
foreach my $device (@devices) {
next if $device->{TYPE} && $device->{TYPE} eq 'cd';
$device->{DISKSIZE} = getDeviceCapacity(
device => '/dev/' . $device->{NAME},
%params
);
}
return @devices;
}
sub _parseUdevEntry {
my (%params) = @_;
my $handle = getFileHandle(%params);
return unless $handle;
my ($result, $serial);
while (my $line = <$handle>) {
if ($line =~ /^S:.*-scsi-(\d+):(\d+):(\d+):(\d+)/) {
$result->{SCSI_COID} = $1;
$result->{SCSI_CHID} = $2;
$result->{SCSI_UNID} = $3;
$result->{SCSI_LUN} = $4;
} elsif ($line =~ /^E:ID_VENDOR=(.*)/) {
$result->{MANUFACTURER} = $1;
} elsif ($line =~ /^E:ID_MODEL=(.*)/) {
$result->{MODEL} = $1;
} elsif ($line =~ /^E:ID_REVISION=(.*)/) {
$result->{FIRMWARE} = $1;
} elsif ($line =~ /^E:ID_SERIAL=(.*)/) {
$serial = $1;
} elsif ($line =~ /^E:ID_SERIAL_SHORT=(.*)/) {
$result->{SERIALNUMBER} = $1;
} elsif ($line =~ /^E:ID_TYPE=(.*)/) {
$result->{TYPE} = $1;
} elsif ($line =~ /^E:ID_BUS=(.*)/) {
$result->{DESCRIPTION} = $1;
}
}
close $handle;
if (!$result->{SERIALNUMBER}) {
$result->{SERIALNUMBER} = $serial;
}
$result->{NAME} = $params{device};
return $result;
}
sub getCPUsFromProc {
my (%params) = (
file => '/proc/cpuinfo',
@_
);
my $handle = getFileHandle(%params);
my (@cpus, $cpu);
while (my $line = <$handle>) {
if ($line =~ /^([^:]+\S) \s* : \s (.+)/x) {
$cpu->{lc($1)} = trimWhitespace($2);
} elsif ($line =~ /^$/) {
# an empty line marks the end of a cpu section
# push to the list, but only if it is a valid cpu
push @cpus, $cpu if $cpu && _isValidCPU($cpu);
undef $cpu;
}
}
close $handle;
# push remaining cpu to the list, if it is valid cpu
push @cpus, $cpu if $cpu && _isValidCPU($cpu);
return @cpus;
}
sub _isValidCPU {
my ($cpu) = @_;
return exists $cpu->{processor} || exists $cpu->{cpu};
}
sub getDevicesFromHal {
my (%params) = (
command => '/usr/bin/lshal',
@_
);
# We need to support dump params to permit full testing when root params is set
if ($params{root}) {
$params{file} = "$params{root}/lshal";
} elsif ($params{dump}) {
$params{dump}->{lshal} = getAllLines(%params);
}
my $handle = getFileHandle(%params);
my (@devices, $device);
while (my $line = <$handle>) {
chomp $line;
if ($line =~ m{^udi = '/org/freedesktop/Hal/devices/(storage|legacy_floppy|block)}) {
$device = {};
next;
}
next unless defined $device;
if ($line =~ /^$/) {
push(@devices, $device);
undef $device;
} elsif ($line =~ /^\s+ storage.serial \s = \s '([^']+)'/x) {
$device->{SERIALNUMBER} = $1;
} elsif ($line =~ /^\s+ storage.firmware_version \s = \s '([^']+)'/x) {
$device->{FIRMWARE} = $1;
} elsif ($line =~ /^\s+ block.device \s = \s '([^']+)'/x) {
my $value = $1;
($device->{NAME}) = $value =~ m{/dev/(\S+)};
} elsif ($line =~ /^\s+ info.vendor \s = \s '([^']+)'/x) {
$device->{MANUFACTURER} = $1;
} elsif ($line =~ /^\s+ storage.model \s = \s '([^']+)'/x) {
$device->{MODEL} = $1;
} elsif ($line =~ /^\s+ storage.drive_type \s = \s '([^']+)'/x) {
$device->{TYPE} = $1;
} elsif ($line =~ /^\s+ storage.size \s = \s (\S+)/x) {
my $value = $1;
$device->{DISKSIZE} = int($value/(1024*1024) + 0.5);
}
}
close $handle;
return @devices;
}
sub getDevicesFromProc {
my (%params) = @_;
# We need to support dump params to permit full testing when root params is set
my $dump = $params{dump};
my $root = $params{root} || "";
my $logger = $params{logger};
# compute list of devices
my @names;
foreach my $file (glob "$root/sys/block/*") {
if ($dump && -d $file) {
my $basename = basename($file);
$dump->{sys}->{block}->{$basename} = {};
}
next unless $file =~ /([shv]d[a-z]+|fd\d)$/;
push @names, $1;
}
# add any block device identified as device by the kernel like SSD disks or
# removable disks (SD cards and others)
foreach my $file (glob "$root/sys/block/*/device") {
if ($dump && -d $file) {
my $dirname = basename(dirname($file));
$dump->{sys}->{block}->{$dirname}->{device} = {};
}
next unless $file =~ m|([^/]*)/device$|;
push @names, $1;
}
foreach my $file (glob "$root/sys/class/scsi_generic/*") {
# block devices should have been handled in the previous step
next if -d "$file/device/block/";
my $basename = basename($file);
if ($dump && -d "$file/device/type") {
my $base = $dump->{sys}->{class}->{scsi_generic}->{$basename} = {};
if (-e "$file/device/type") {
my $content = getAllLines(file => "$file/device/type");
$base->{device}->{type} = $content;
}
}
my $type = getFirstLine(
file => "$file/device/type",
logger => $logger
);
# if not disk
next if (!defined($type) || $type != 0);
push @names, $basename;
}
# filter duplicates
my %seen;
@names = grep { !$seen{$_}++ } @names;
# filter multipath
@names = _filterMultipath(names => \@names) if canRun('multipath');
my $udisksctl = canRun('udisksctl');
$dump->{udisksctl} = 1 if ($dump && $udisksctl);
$udisksctl = 1 if $root && -e "$root/udisksctl";
# extract information
my @devices;
foreach my $name (@names) {
my $device = {
NAME => $name,
MANUFACTURER => _getValueFromSysProc($logger, $name, 'vendor', $root, $dump),
MODEL => _getValueFromSysProc($logger, $name, 'model', $root, $dump),
FIRMWARE => _getValueFromSysProc($logger, $name, 'rev', $root, $dump)
|| _getValueFromSysProc($logger, $name, 'firmware_rev', $root, $dump),
SERIALNUMBER => _getValueFromSysProc($logger, $name, 'serial', $root, $dump)
|| _getValueFromSysProc($logger, $name, 'vpd_pg80', $root, $dump),
TYPE =>
_getValueFromSysProc($logger, $name, 'removable', $root, $dump) ?
'removable' : 'disk'
};
# WWN
my $wwn = _getValueFromSysProc($logger, $name, 'wwid', $root, $dump);
$device->{WWN} = $wwn if $wwn && $wwn =~ s/^naa\./wwn-/;
# Support PCI or other bus case as description
foreach my $subsystem ("device/subsystem","device/device/subsystem") {
my $link = _readLinkFromSysFs("/sys/block/$name/$subsystem", $root, $dump);
next unless ($link && $link =~ m|^/sys/bus/(\w+)$|);
$device->{DESCRIPTION} = uc($1);
last;
}
# Support disk size from /sys/block
my $size_by_sectors = _getValueFromSysProc($logger, $name, 'size', $root, $dump);
if ($size_by_sectors) {
$device->{DISKSIZE} = int($size_by_sectors * 512 / 1_000_000);
}
# Check removable capacity as HintAuto via udiskctl while available
if ($udisksctl && $device->{TYPE} eq 'disk') {
my %match = (
pattern => qr/^\s+HintAuto:\s+(true|false)$/,
logger => $logger
);
if ($root) {
$match{file} = "$root/udisksctl-$name";
} else {
$match{command} = "udisksctl info -b /dev/$name";
}
$dump->{"udisksctl-$name"} = getAllLines(%match) if ($dump);
my $hintauto = getFirstMatch(%match);
$device->{TYPE} = 'removable'
if ( $hintauto && $hintauto eq 'true' );
}
push @devices, $device;
}
return @devices;
}
sub _getValueFromSysProc {
my ($logger, $device, $key, $root, $dump) = @_;
## no critic (ExplicitReturnUndef)
my $file = first { -f $root.$_ }
"/sys/block/$device/$key",
"/sys/block/$device/device/$key",
"/proc/ide/$device/$key",
"/sys/class/scsi_generic/$device/device/$key";
return undef unless $file;
# We need to support dump params to permit full testing when root params is set
if ($dump) {
foreach my $sub (split('/',dirname($file))) {
next unless $sub;
$dump->{$sub} = {} unless $dump->{$sub};
$dump = $dump->{$sub};
}
$dump->{basename($file)} = getAllLines(file => $file);
}
my $value = getFirstLine(file => $root.$file, logger => $logger);
return undef unless defined $value;
$value = $1 if $value =~ /([[:print:]]*[[:alnum:]]+)/;
return trimWhitespace($value);
}
sub _filterMultipath {
my (%params) = (
command => 'multipath -l',
@_
);
my %hnames = map {$_ => 1} @{$params{names}};
# parse multipath
my (@groups, @group);
foreach my $line (getAllLines(%params)) {
if ($line =~ /^\w+ \(\w+\) dm-\d+ \w+/) {
push @groups, [@group] if scalar @group;
@group = ();
} elsif ($line =~ / (sd[a-z]+) /) {
push @group, $1;
}
}
push @groups, [@group] if scalar @group;
# filter
foreach my $group (@groups) {
# delete all but first element from each group
for (my $i = 1; $i < scalar @$group; $i++) {
delete $hnames{ @$group[$i] } ;
}
}
return keys %hnames;
}
sub _readLinkFromSysFs {
my ($path, $root, $dump) = @_;
## no critic (ExplicitReturnUndef)
my @path = split('/', $path);
return undef unless (!shift(@path) && shift(@path) eq 'sys');
my @sys = ();
# We need to support dump params to permit full testing when root params is set
if ($dump) {
foreach my $sub (split('/',dirname($path))) {
next unless $sub;
$dump->{$sub} = {} unless $dump->{$sub};
$dump = $dump->{$sub};
}
$dump->{basename($path)} = [ link => readlink($path) ];
}
while (@path) {
push @sys, shift(@path);
my $link = readlink($root.'/sys/'.join('/', @sys));
next unless $link;
pop @sys;
foreach my $sub (split('/',$link)) {
if ($sub eq '..') {
pop @sys;
} else {
push @sys, $sub;
}
}
}
return '/sys/'.join('/', @sys);
}
sub getInfoFromSmartctl {
my (%params) = @_;
# We need to support dump params to permit full testing when root params is set
$params{file} = "$params{root}/smartctl-".basename($params{device})
if $params{root};
my @lines = getAllLines(
%params,
command => $params{device} ?
"smartctl -i $params{device} " . ($params{extra} // "") : undef,
);
# We need to support dump params to permit full testing when root params is set
$params{dump}->{"smartctl-".basename($params{device})} = @lines ? join("\n", @lines)."\n" : ""
if $params{dump};
return unless @lines;
my $info = {
TYPE => 'disk',
DESCRIPTION => 'SATA',
};
my $attrs = {
DESCRIPTION => {
src => ['transport protocol']
},
DISKSIZE => {
src => ['user capacity'],
func => \&getCanonicalSize,
},
FIRMWARE => {
src => ['revision', 'firmware version']
},
INTERFACE => {
src => ['transport protocol', 'sata version is']
},
MANUFACTURER => {
src => ['vendor', 'model family', 'add. product id', 'device model', 'product'],
func => \&getCanonicalManufacturer,
},
MODEL => {
src => ['product', 'device model', 'model family']
},
SERIALNUMBER => {
src => ['serial number']
},
TYPE => {
src => ['device type']
},
WWN => {
src => ['lu wwn device id'],
# remove whitespaces
func => sub { shift =~ s/\s+//gr },
}
};
my $regexp = {
__default => qr/^(\w+)/,
__smartctl => qr/^([^:]+?)\s*:\s*(.+)\s*$/,
'user capacity' => qr/([\d\.\,\s]+(?:\w+)?)/,
'device model' => qr/([\w\s\-]+)/,
'firmware version' => qr/(\S+)/,
'lu wwn device id' => qr/((?:0x)?[[:xdigit:]\h]+)/,
};
my %smartctl;
for my $line (@lines) {
next unless $line =~ $regexp->{__smartctl};
$smartctl{lc $1} = $2;
}
while (my ($attr, $val) = each %$attrs) {
for my $s (@{$val->{src}}) {
next unless defined $smartctl{$s};
my ($data) = ($smartctl{$s} =~ ($regexp->{$s} // $regexp->{__default}));
$info->{$attr} = exists $val->{func} ? &{$val->{func}}($data) : $data;
last;
}
}
return $info;
}
sub getInterfacesFromIfconfig {
my (%params) = (
command => '/sbin/ifconfig -a',
@_
);
my $handle = getFileHandle(%params);
return unless $handle;
my @interfaces;
my $interface;
my %types = (
Ethernet => 'ethernet',
);
while (my $line = <$handle>) {
if ($line =~ /^$/) {
# end of interface section
push @interfaces, $interface if $interface;
next;
}
if ($line =~ /^([\w\d.]+)/) {
# new interface
$interface = {
STATUS => 'Down',
DESCRIPTION => $1
}
}
if ($line =~ /
inet \s ($ip_address_pattern) \s+
netmask \s ($ip_address_pattern) \s+
broadcast \s $ip_address_pattern
/x) {
$interface->{IPADDRESS} = $1;
$interface->{IPMASK} = $2;
}
if ($line =~ /
ether \s ($mac_address_pattern)
.+
\( Ethernet \)
/x) {
$interface->{MACADDR} = $1;
$interface->{TYPE} = 'ethernet';
}
if ($line =~ /inet6 \s (\S+)/x) {
$interface->{IPADDRESS6} = $1;
}
if ($line =~ /inet addr:($ip_address_pattern)/i) {
$interface->{IPADDRESS} = $1;
}
if ($line =~ /Mask:($ip_address_pattern)/) {
$interface->{IPMASK} = $1;
}
if ($line =~ /inet6 addr: (\S+)/i) {
$interface->{IPADDRESS6} = $1;
}
if ($line =~ /hwadd?r\s+($mac_address_pattern)/i) {
$interface->{MACADDR} = $1;
}
if ($line =~ /^\s+UP\s/) {
$interface->{STATUS} = 'Up';
}
if ($line =~ /flags=.*[<,]UP[>,]/) {
$interface->{STATUS} = 'Up';
}
if ($line =~ /Link encap:(\S+)/) {
$interface->{TYPE} = $types{$1};
}
}
close $handle;
return @interfaces;
}
sub getInterfacesInfosFromIoctl {
my (%params) = (
interface => 'eth0',
@_
);
return unless $params{interface};
my $logger = $params{logger};
socket(my $socket, PF_INET, SOCK_DGRAM, 0)
or return ;
# Pack command in ethtool_cmd struct
my $cmd = pack("L3SC6L2SC2L3", ETHTOOL_GSET);
# Pack request for ioctl
my $request = pack("a16p", $params{interface}, $cmd);
my $retval = ioctl($socket, SIOCETHTOOL, $request) || -1;
return if ($retval < 0);
# Unpack returned datas
my @datas = unpack("L3SC6L2SC2L3", $cmd);
# Actually only speed value is requested and extracted
my $datas = {
SPEED => $datas[3]|$datas[12]<<16
};
# Forget speed value if got unknown speed special value
if ($datas->{SPEED} == SPEED_UNKNOWN) {
delete $datas->{SPEED};
$logger->debug2("Unknown speed found on $params{interface}")
if $logger;
}
return $datas;
}
sub getInterfacesFromIp {
my (%params) = (
command => '/sbin/ip addr show',
@_
);
my $handle = getFileHandle(%params);
return unless $handle;
my (@interfaces, @addresses, $interface);
while (my $line = <$handle>) {
if ($line =~ /^\d+:\s+(\S+): <([^>]+)>/) {
if (@addresses) {
push @interfaces, @addresses;
undef @addresses;
} elsif ($interface) {
push @interfaces, $interface;
}
my ($name, $flags) = ($1, $2);
my $status =
(any { $_ eq 'UP' } split(/,/, $flags)) ? 'Up' : 'Down';
$interface = {
DESCRIPTION => $name,
STATUS => $status
};
} elsif ($line =~ /link\/\S+ ($any_mac_address_pattern)?/) {
$interface->{MACADDR} = $1;
} elsif ($line =~ /inet6 (\S+)\/(\d{1,2})/) {
my $address = $1;
my $mask = getNetworkMaskIPv6($2);
my $subnet = getSubnetAddressIPv6($address, $mask);
push @addresses, {
IPADDRESS6 => $address,
IPMASK6 => $mask,
IPSUBNET6 => $subnet,
STATUS => $interface->{STATUS},
DESCRIPTION => $interface->{DESCRIPTION},
MACADDR => $interface->{MACADDR}
};
} elsif ($line =~ /
inet \s
($ip_address_pattern)(?:\/(\d{1,3}))? \s
.* \s
(\S+)$
/x) {
my $address = $1;
my $mask = getNetworkMask($2);
my $subnet = getSubnetAddress($address, $mask);
my $name = $3;
# the name associated with the address differs from the current
# interface if the address is actually attached to an alias
push @addresses, {
IPADDRESS => $address,
IPMASK => $mask,
IPSUBNET => $subnet,
STATUS => $interface->{STATUS},
DESCRIPTION => $name,
MACADDR => $interface->{MACADDR}
};
}
}
close $handle;
if (@addresses) {
push @interfaces, @addresses;
undef @addresses;
} elsif ($interface) {
push @interfaces, $interface;
}
return @interfaces;
}
1;
__END__
=head1 NAME
FusionInventory::Agent::Tools::Linux - Linux generic functions
=head1 DESCRIPTION
This module provides some generic functions for Linux.
=head1 FUNCTIONS
=head2 getDevicesFromUdev(%params)
Returns a list of devices, by parsing /dev/.udev directory.
This directory is not exported anymore with recent udev.
Availables parameters:
=over
=item logger a logger object
=back
=head2 getDevicesFromHal(%params)
Returns a list of devices, by parsing lshal output.
Availables parameters:
=over
=item logger a logger object
=item command the exact command to use (default: /usr/sbin/lshal)
=item file the file to use, as an alternative to the command
=back
=head2 getDevicesFromProc(%params)
Returns a list of devices, by parsing /proc filesystem.
Availables parameters:
=over
=item logger a logger object
=back
=head2 getCPUsFromProc(%params)
Returns a list of cpus, by parsing /proc/cpuinfo file
Availables parameters:
=over
=item logger a logger object
=item file the file to use (default: /proc/cpuinfo)
=back
=head2 getInfoFromSmartctl(%params)
Returns some information about a device, using smartctl.
Availables parameters:
=over
=item logger a logger object
=item device the device to use
=item file the file to use
=back
=head2 getInterfacesFromIfconfig(%params)
Returns the list of interfaces, by parsing ifconfig command output.
Availables parameters:
=over
=item logger a logger object
=item command the command to use (default: /sbin/ifconfig -a)
=item file the file to use
=back
=head2 getInterfacesInfosFromIoctl(%params)
Returns interface datas, by parsing results from ethtool system call request.
Availables parameters:
=over
=item logger a logger object
=item interface the interface name to use (default: eth0)
=back
=head2 getInterfacesFromIp(%params)
Returns the list of interfaces, by parsing ip command output.
Availables parameters:
=over
=item logger a logger object
=item command the command to use (default: /sbin/ip addr show)
=item file the file to use
=back