~ K A L I ~
UNAME : Linux web65.extendcp.co.uk 4.18.0-553.56.1.el8_10.x86_64 #1 SMP Tue Jun 10 05:00:59 EDT 2025 x86_64SERVER IP : 10.0.187.65 -________-
CLIENT IP : 216.73.216.230 MINI SHELL D ZAB '
Current File : //usr/bin/glpi-remote |
#!/usr/bin/perl
use strict;
use warnings;
use lib "/usr/share/glpi-agent/lib";
use setup;
use UNIVERSAL::require;
use English qw(-no_match_vars);
use LWP::UserAgent;
use Pod::Usage;
use Getopt::Long;
use Digest::SHA;
use Time::HiRes qw(gettimeofday);
use File::Which;
use File::Find;
use GLPI::Agent::Config;
use GLPI::Agent::Logger;
use GLPI::Agent::Storage;
use GLPI::Agent::XML;
use GLPI::Agent::Task::RemoteInventory::Remote;
our $VERSION = "1.0";
Getopt::Long::Configure( "no_ignorecase" );
Getopt::Long::Configure( "pass_through" );
my $logger;
my $options = {
useragent => "GLPI-Remote/$VERSION",
timeout => 10,
};
GetOptions(
$options,
'help|h',
'useragent|U=s',
'verbose|v',
'debug+',
'inventory|i',
'target=s',
'port|p=s',
'timeout|t=i',
'threads|T=i',
'user|u=s',
'password|P=s',
'show-passwords|X',
'credentials|c=s',
'baseurl|b=s',
'token|K=s',
'directory|d=s',
'id|I=s',
'no-compression',
'add|A',
'ssh',
'ssl',
'ca-cert-file=s',
'ca-cert-dir=s',
'ssl-cert-file=s',
'ssl-fingerprint=s',
'stricthostkeychecking=s',
'no-ssl-check|S',
'no-check|C',
'no-header|H',
'vardir=s',
) or pod2usage(-verbose => 0);
pod2usage(-verbose => 0, -exitstatus => 0) if $options->{help};
if ($options->{vardir}) {
die "Wrong vardir option: $!\n" unless -d $options->{vardir};
$setup{vardir} = $options->{vardir};
} else {
$options->{vardir} = $setup{vardir};
}
my ($cmd, @params) = @ARGV;
if ($cmd) {
if ($options->{debug}) {
warn "Current vardir: $setup{vardir}\n";
}
die "Var directory is missing\n" unless -d $setup{vardir};
my %cmds = (
agent => \&agent,
list => \&list,
help => \&help,
add => \&add,
del => \&delete,
delete => \&delete,
scan => \&todo,
);
die "Unsupported '$cmd' command\n" unless exists($cmds{$cmd});
$logger = GLPI::Agent::Logger->new(config => $options);
&{$cmds{$cmd}}(@params);
exit(0);
}
pod2usage(-verbose => 0, -exitstatus => 0);
exit(0);
my %deviceid;
my %targets;
my %storages;
sub help {
my @params = @_;
pod2usage(-verbose => 0, -exitstatus => 0);
}
sub todo {
die "TODO: command '$cmd' still not supported\n";
}
sub list {
my ($sub) = @_;
my ($lenid, $lenurl) = (0, 0);
if ($sub) {
die "Unsupported '$sub' list subcommand\n" unless $sub eq "targets";
my @targets = get_targets();
my ($lentype, $lentime) = (0, 0);
map { my $l = length($_); $lentype = $l if $l > $lentype } "type", map { $_->{type} } @targets;
map { my $l = length($_); $lenid = $l if $l > $lenid } "id", map { $_->{id} } @targets;
map { my $l = length($_); $lenurl = $l if $l > $lenurl } "url/path", map { $_->{url} // $_->{path} // '' } @targets;
map { my $l = length($_); $lentime = $l if $l > $lentime } "maxdelay", map { $_->{maxDelay} // 0 } @targets;
my $string = "%-".$lenid."s %-".$lentype."s %-".$lenurl."s %-".$lentime."s %s\n";
print sprintf( $string, "id", "type", "url/path", "maxdelay", "Next run date")
unless $options->{'no-header'};
foreach my $target (@targets) {
print sprintf( $string,
$target->{id}, $target->{type}, $target->{url} || $target->{path} // '',
$target->{maxDelay}, scalar(localtime($target->{nextRunDate}//0))
);
}
} else {
my @remotes = get_remotes();
my $lentg = 0;
map { my $l = length($_); $lenid = $l if $l > $lenid } "deviceid", map { $_->deviceid } map { $_->{remote} } @remotes;
map { my $l = length($_); $lenurl = $l if $l > $lenurl } "url", map { $_->url } map { $_->{remote} } @remotes;
map { my $l = length($_); $lentg = $l if $l > $lentg } "target", map { $_->{id} } map { $_->{target} } @remotes;
my $lenindex = length(scalar(@remotes));
$lenindex = length("index") if length("index") > $lenindex;
my $string = "%".$lenindex."s %-".$lenid."s %-".$lenurl."s %-".$lentg."s %s\n";
print sprintf( $string, "index", "deviceid", "url", "target", "Next run date")
unless $options->{'no-header'};
my $index = 1;
foreach my $current (@remotes) {
my $remote = $current->{remote};
print sprintf( $string,
$index++, $remote->deviceid, $remote->safe_url,
$current->{target}->{id},
$remote->expiration ? scalar(localtime($remote->expiration)) : "on next agent run"
);
}
}
}
sub add {
my @urls = @_;
# Look at targets and collect target storages
my @targets = get_targets();
# Try to preselect server if no target option is specified
unless ($options->{target} || $options->{server} || $options->{local}) {
my @servers = grep { $_->{type} eq 'server' } @targets;
warn "Can't select a default server target\n" unless @servers == 1;
$options->{target} = $servers[0]->{id} if @servers == 1;
}
my @storages;
if ($options->{target}) {
my $target = $options->{target};
die "No such '$target' target available to handle remotes\n" unless $targets{$target};
my $storage = $targets{$target}->{storage};
if ($storage) {
push @storages, $storage;
$storages{$storage} = $target;
}
}
if ($options->{server} || $options->{local}) {
my $config = GLPI::Agent::Config->new(
options => $options,
);
my $targets = $config->getTargets();
foreach my $target (@{$targets}) {
next unless $target->isType('local') || $target->isType('server');
my $storage = $target->getStorage();
next unless $storage;
push @storages, $storage;
$storages{$storage} = $target->{id};
}
}
unless (@storages) {
my $targets = join(", ", sort keys(%targets));
die "No target specified to store remotes, choose one in $targets\n" ;
}
my $umask = umask 0007;
foreach my $url (@urls) {
my $remote = GLPI::Agent::Task::RemoteInventory::Remote->new(
url => $url,
config => $options,
logger => $logger,
);
$remote->supported()
or next;
$remote->prepare();
# Eventually add StrictHostKeyChecking ssh options
$remote->options(["StrictHostKeyChecking=".$options->{'stricthostkeychecking'}])
if $options->{'stricthostkeychecking'} && $remote->protocol() eq 'ssh';
my $checkerror;
$checkerror = $remote->checking_error() unless $options->{'no-check'};
if ($checkerror) {
my $safe_url = $remote->safe_url();
warn "'$safe_url' check failure: $checkerror\n";
next;
}
my $id = $remote->deviceid();
foreach my $storage (@storages) {
my $remotes = $storage->restore( name => 'remotes' ) // {};
my $match;
if ($remotes->{$id}) {
my $targetid = $storages{$storage} // '';
warn "Updating $id remote".($targetid ? " for $targetid" : "")."\n";
$match = $id;
} elsif ($remote->protocol() eq 'winrm') {
foreach my $otherid (sort keys(%{$remotes})) {
my $other = GLPI::Agent::Task::RemoteInventory::Remote->new(
url => $remotes->{$otherid}->{url},
);
next unless $other->protocol() eq 'winrm';
next unless $other->winrm_url() eq $remote->winrm_url();
$match = $otherid;
warn "Updating $match remote for $storages{$storage}\n";
last;
}
}
$remote->deviceid(deviceid => $match) if $match;
$remotes->{$match||$id} = $remote->dump();
$storage->save( name => 'remotes', data => $remotes );
if ($match) {
print "Updated $match remote agent for $storages{$storage}\n";
} else {
print "Added $id remote agent to $storages{$storage}\n";
}
}
}
umask $umask;
}
sub delete {
die "No index or deviceid provided to delete command\n" unless @_;
my @remotes = get_remotes();
foreach my $rm (@_) {
my $current;
if ($rm =~ /^\d+$/ && $rm <= @remotes) {
$current = [ $remotes[$rm-1] ];
} elsif ($deviceid{$rm}) {
$current = $deviceid{$rm};
} else {
warn "No such remote: $rm\n";
next;
}
foreach my $list (@{$current}) {
my $storage = $list->{storage};
my $remotes = $storage->restore( name => 'remotes' ) // {};
my $id = $list->{remote}->deviceid();
($id) = grep { $remotes->{$_}->{deviceid} eq $id } keys(%{$remotes})
unless $remotes->{$id};
my $deleted = delete $remotes->{$id}
or warn "No such deviceid found: $id\n";
if (keys(%{$remotes})) {
$storage->save( name => 'remotes', data => $remotes );
} else {
$storage->remove( name => 'remotes' );
}
print "$id remote agent deleted from $list->{target}->{id}\n" if $deleted;
}
}
}
sub get_remotes {
my @remotes;
my @targets = get_targets();
foreach my $target (@targets) {
my $id = $target->{id};
my $current = $targets{$id}
or next;
my $storage = $current->{storage};
# Load remotes from storage
my $remotes = $storage->restore( name => 'remotes' ) // {};
foreach my $id (sort keys(%{$remotes})) {
my $dump = $remotes->{$id};
next unless ref($dump) eq 'HASH';
my $remote = GLPI::Agent::Task::RemoteInventory::Remote->new(
dump => $dump,
config => $options,
logger => $logger,
);
$remote->supported()
or next;
my $current = {
remote => $remote,
storage => $storage,
target => $target,
};
push @remotes, $current;
# Also index by deviceid when deleting
if ($cmd =~ /^del(?:ete)?$/) {
push @{$deviceid{$id}}, $current;
}
}
}
return @remotes;
}
sub get_targets {
my @targets;
File::Find::find(
{
wanted => sub {
return unless $File::Find::name =~ m/\/target\.dump$/;
return if -d $File::Find::name;
my $storage = GLPI::Agent::Storage->new(
directory => $File::Find::dir,
logger => $logger,
read_only => 1,
);
# Check target from storage
my $target = $storage->restore( name => 'target' ) // {};
return unless $target->{type} && $target->{id};
push @targets, $target;
# Also keep storage refs when adding a remote to a local or server target
if ($target->{type} =~ /^local|server$/ ) {
my $current = {
target => $target,
storage => $storage,
};
my $id = $target->{id};
# Update id number in the case the id is still used
if ($targets{$id}) {
my ($base, $num) = $id =~ /^(\w+)(\d+)$/;
while ($targets{$id}) {
$id = $base . (++$num);
}
$target->{id} = $id;
}
$targets{$id} = $current;
}
},
no_chdir => 1
},
$setup{vardir}
);
return @targets;
}
sub agent {
my @hosts = @_;
pod2usage(
-message => "\nGive a least one host to get inventory from as parameter\n",
-verbose => 0,
-exitstatus => 1
) unless @hosts;
pod2usage(
-message => "\nNo token as shared secret defined\n",
-verbose => 0,
-exitstatus => 1
) unless $options->{token};
pod2usage(
-message => "\nWhen asking inventory to more than one host, you must use the --directory parameter\n",
-verbose => 0,
-exitstatus => 1
) if !$options->{directory} && @hosts>1;
pod2usage(
-message => "\nDirectory not found: $options->{directory}\n",
-verbose => 0,
-exitstatus => 1
) if ($options->{directory} && ! -d $options->{directory});
my $ua = LWP::UserAgent->new(
agent => $options->{useragent},
timeout => $options->{timeout} || 180,
parse_head => 0, # No need to parse HTML
keep_alive => 1,
);
if ($options->{ssl}) {
$ua->ssl_opts(SSL_ca_file => $options->{'ca-cert-file'})
if $options->{'ca-cert-file'};
$ua->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0)
if $options->{ssl} && $options->{'no-ssl-check'};
}
$options->{verbose} = 1 if $options->{debug};
my $id = $options->{id} || _id();
warn "Using $id as request id\n"
if $options->{verbose};
foreach my $host (@hosts) {
my $url = ( $options->{ssl} ? "https://" : "http://" ). $host;
$url .= $options->{port} ? ":".$options->{port} : ":62354";
$url .= $options->{baseurl} ? $options->{baseurl} : "/inventory";
warn "$host: Trying $url\n"
if $options->{verbose};
my $req = HTTP::Request->new(GET => $url.'/session');
$req->header( 'X-Request-ID' => $id );
$req->protocol( 'HTTP/1.1' );
if ($options->{debug}) {
warn "--->\n";
warn "Request: ".$req->as_string();
}
my $session = $ua->request($req);
if ($options->{debug}) {
warn "<---\n";
warn "Response: ".$session->as_string();
}
if (!$session->is_success()) {
warn "$host: No session (".$session->status_line().")\n";
next;
}
my $nonce = $session->header('X-Auth-Nonce')
or die "No nonce\n";
my $sha = Digest::SHA->new(256);
$sha->add($nonce.'++'.$options->{token});
my $payload = $sha->b64digest;
# Update request to get inventory
$req->uri($url.'/get');
$req->header( 'X-Auth-Payload' => $payload );
# Set Accept header
my $accept = 'application/xml';
if (!$options->{'no-compression'}) {
$accept .= ', application/x-compress-zlib'
if Compress::Zlib->require();
my $zcat = scalar(which('zcat'));
$accept .= ', application/x-compress-gzip'
if -x $zcat;
}
$req->header('Accept' => $accept);
if ($options->{debug}) {
warn "--->\n";
warn "Request: ".$req->as_string();
}
my $xml = $ua->request($req);
if ($options->{debug}) {
warn "<---\n";
warn "Response: ".$xml->status_line()."\n".$xml->headers->as_string()."\n";
}
if (!$xml->is_success()) {
warn "$host: Inventory request: ".$xml->status_line()."\n";
next;
}
my $content = $xml->content();
# check compression mode
if ($xml->header('Content-Type') eq 'application/x-compress-zlib') {
# RFC 1950
warn "$host: Using Compress::Zlib for decompression\n"
if $options->{debug};
$content = Compress::Zlib::uncompress($content);
} elsif ($xml->header('Content-Type') eq 'application/x-compress-gzip') {
# RFC 1952
warn "$host: Using gzip for decompression\n"
if $options->{debug};
File::Temp->require();
my $fd = File::Temp->new();
print $fd $content;
close $fd;
my $OUT;
unless(open $OUT, '-|', 'zcat ' . $fd->filename()) {
warn "$host: Failed to uncompress response, skipping\n";
next;
}
local $INPUT_RECORD_SEPARATOR; # Set input to "slurp" mode.
$content = <$OUT>;
close($OUT);
}
my $deviceid;
$xml = GLPI::Agent::XML->new(string => $content);
my $tree = $xml->dump_as_hash();
if ($tree && $tree->{REQUEST} && $tree->{REQUEST}->{DEVICEID}) {
$deviceid = $tree->{REQUEST}->{DEVICEID};
} else {
warn "$host: No deviceid found in returned inventory output, skipping\n";
next;
}
warn "$host: Got remote inventory from $deviceid\n"
if $options->{verbose};
if ($options->{directory}) {
my $filename = $options->{directory}."/$deviceid.xml";
open my $FILE, ">", $filename
or die "$host: Can't open $filename: $!\n";
print $FILE $content;
close($FILE);
warn "$host: Written inventory in $filename\n"
if $options->{verbose};
} else {
print $content;
}
}
}
# Compute a simple and as-possible safe id
sub _id {
my $sha = Digest::SHA->new(1);
$sha->add(gettimeofday());
$sha->add(gettimeofday());
my $digest = $sha->hexdigest;
return substr($digest, 0, 8);
}
__END__
=head1 NAME
glpi-remote - A tool to scan, manage and initialize virtual remote agents
=head1 SYNOPSIS
glpi-remote [options] [--server server|--local path] [command] [command options]
Options:
-h --help this menu
-t --timeout <SECS> requests timeout in seconds (defaults to 10)
-p --port <LIST> remote ports list to scan (defaults to '22,5985,5986')
--ssh connect using SSH
--ssl connect using SSL (winrm or with agent sub-command)
--no-ssl-check do not check agent SSL certificate (winrm or agent sub-command)
--stricthostkeychecking <yes|no|off|accept-new|ask> (defaults to 'accept-new')
use given option when checking hostkey during ssh remote add
--ca-cert-dir <PATH> CA certificates directory
--ca-cert-file <FILE> CA certificates file (winrm or for agent sub-command)
--ssl-fingerprint <FINGERPRINT>
Trust server certificate if its SSL fingerprint matches the given one
--ssl-cert-file Client certificate file (winrm)
-u --user authentication user
-P --password authentication password
-X --show-passwords (list command) show password as they are masked by default
-c --credentials credentials list for scan
-v --verbose verbose mode
--debug debug mode
-C --no-check don't check given remote is alive when adding
-i --inventory don't register remotes, but run inventory on found remotes
-T --threads <NUM> number of threads while scanning (defaults to 1)
-A --add add scanned remotes to target so they always be inventoried
by RemoteInventory task
-U --useragent set used HTTP User-Agent for requests
--vardir <PATH> use specified path as storage folder for agent persistent datas
Target definition options:
-s --server=<URI> agent will send tasks result to that server
-l --local=<PATH> agent will write tasks results locally
--target=<TARGETID> use target identified by its id (see list targets command)
Remote GLPI agent having inventory server plugin enabled options:
-b --baseurl <PATH> remote base url if not /inventory
-K --token <TOKEN> token as shared secret
-I --id <ID> request id to identify requests in agent log
--no-compression ask to not compress sent XML inventories
Sub-commands
list [targets] list known remotes or targets
add <url>+ add remote with given URL list
del[ete] <index|deviceid>+
delete remote with given list index or given deviceid or
current known one when alone or all remotes while using
__ALL__ as id
scan <first> [last] [TODO] scan given ip range for remote access or just <first> and
register it/them as remote agent
agent [hosts] remotely claim an inventory to given remote hosts with a
GLPI agent having inventory server plugin enabled
(see https://glpi-agent.rtfd.io/inventory-server-plugin.html)
Supported environment variables:
USERNAME
PASSWORD
PORT
CA_CERT_PATH
CA_CERT_FILE
SSL_CERT_FILE
CREDENTIALS
Examples:
glpi-remote list
glpi-remote list targets
glpi-remote add ssh://admin:pass@192.168.43.237
glpi-remote add ssh://admin:pass@192.168.43.237 --stricthostkeychecking=no
glpi-remote add ssh://admin:pass@192.168.43.238 --no-check
glpi-remote add winrm://admin:pass@192.168.48.250 --no-check --target server0
glpi-remote delete 1
glpi-remote scan 192.168.43.1 192.168.43.254
glpi-remote scan 10.0.0.1 10.0.10.254 --inventory -s https://myglpi/
glpi-remote scan 10.0.0.1 10.0.10.254 --inventory -l /var/tmp/remotes
glpi-remote scan --inventory
glpi-remote scan 192.168.48.99 | glpi-injector -url https://myglpi/
Examples for agent command:
glpi-remote -T strong-shared-secret agent 192.168.43.236
glpi-remote -v -T strong-shared-secret agent 192.168.43.237 | \
glpi-injector -url https://myglpi/
glpi-remote -T strong-shared-secret -d /var/remote agent 192.168.43.236 192.168.43.237
=head1 DESCRIPTION
The F<glpi-remote> tool is used to manage virtual agents known locally by F<glpi-agent>.
A virtual agent is used to make remote inventories and is essentially defined by
a remote access. A remote access can be defined by ssh authorization for unix/linux
platforms or WinRM authorizations for a WinRM enabled platform like win32.
=head1 OPTIONS
Most of the options are available in a I<short> form and a I<long> form. For
example, the two lines below are all equivalent:
% glpi-agent -s localhost
% glpi-agent --server localhost
=head2 Target definition options
=over
=item B<-s>, B<--server>=I<URI>
Send the results of tasks execution to given server.
Multiple values can be specified, using comma as a separator.
=item B<-l>, B<--local>=I<PATH>
Write the results of tasks execution locally.
=item B<--target>=I<TARGETID>
Use the given TARGETID to look for the expected target for result submission.
For example, B<server0> is the first server target setup in agent configuration.
=back
B<Remark:>
=over
=item *
target option is generaly mandatory while adding remote or scanning for remotes
=item *
if one server and only one is still setup in the agent it will be selected as default target
=item *
when scanning and making inventory, uses any target option or each inventory will be sent to standard output
=back
=head2 General options
=over
=item B<-t>, B<--timeout>=I<SECS>
Set the timeout for network requests (defaults to 10 seconds).
=item B<-p>, B<--port>=I<LIST>
A list of ports used when making a scan and to discover remote computers. The defaults is to
scan the standard ssh port and winrm ports: I<22,5985,5986>.
=item B<--ssh>
Use ssh protocol for connection.
=item B<--ssl>
Use SSL protocol for connecting with WinRM protocol or to a remote agent with inventory server
plugin enabled.
=item B<--ca-cert-dir>=I<DIRECTORY>
CA certificates directory.
=item B<--ca-cert-file>=I<FILE>
CA certificates file.
=item B<--ssl-cert-file>=I<FILE>
SSL certificate file for authentication
=item B<--no-ssl-check>
Do not check server SSL certificate.
=item B<-u> I<USER>, B<--user>=I<USER>
Use I<USER> for remote authentication.
=item B<-P>, B<--password>=I<PASSWORD>
Use I<PASSWORD> for remote authentication.
=item B<-X>, B<show-passwords>
By default, B<list> sub-command won't show remotes passwords. This option asks to unmask
them during remotes listing.
=item B<-c>, B<--credentials>=I<LIST>
List of credentials to try during a scan.
=item B<-v>, B<--verbose>
Enable verbose mode.
=item B<--debug>
Turn the debug mode on. You can use the parameter up to 2 times in a row
to increase the verbosity (e.g: B<--debug --debug>).
=item B<-C>, B<--no-check>
Don't check remote is alive while adding it.
=item B<-i>, B<--inventory>
Don't register remotes as they are discovered but just run the inventory task on them.
=item B<-T>, B<--threads>=I<NUM>
Setup number of threads while doing a scan. By default, the agent only uses one thread.
=item B<-A>, B<--add>
Add discovered remotes to local remotes list.
=item B<-U>, B<--useragent>=I<USER-AGENT>
Define HTTP user agent for request (mostly if required for winrm).
=back
=head2 I<agent> sub-command options
=over
=item B<-b>, B<--baseurl>=I<PATH>
Remote base url if the default I</inventory> has been changed in the remote plugin configuration.
=item B<-K>, B<--token>=I<TOKEN>
Shared secret required to request an inventory to the remote plugin.
=item B<-I>, <--id>=I<ID>
Request-ID to identify the request in the agent log.
=item B<--no-compression>
Ask to skip requested inventory compression.
=back
=head2 Sub-commands
=over
=item * B<list> [B<targets>]
list known remotes or list targets
=item * B<add> I<url>+
add remote with given URL list
=item * B<del[ete]> I<index|deviceid>+
Delete remote with given:
=over
=item * list index
=item * given deviceid
=item * current and only one known when no index is given
=item * all known remotes while using the B<__ALL__> magic word as index
=back
=item * B<scan> I<first> [I<last>]
B<TODO:> I<This sub-command is still not implemented>.
Scan given ip range for remote access or just I<first> and register it/them as remote agent
=item * B<agent> [I<hosts>]
Remotely claim an inventory to given remote hosts with a GLPI agent having inventory server plugin enabled.
See online documentation for details: https://glpi-agent.rtfd.io/inventory-server-plugin.html
=back
=head2 Environment variables
For security reasons, you can set few environment variables to store sensible datas.
=over
=item * B<USERNAME> to setup connection user
=item * B<PASSWORD> to setup connection password
=item * B<PORT> to setup connection port
=item * B<CA_CERT_PATH>
=item * B<CA_CERT_FILE> to setup the SSL CA certificate file
=item * B<SSL_CERT_FILE> to setup the SSL client certificate file
=item * B<CREDENTIALS> to setup a list of credentials
=back
Coded by KALI :v Greetz to DR HARD ../ kali.zbi@hotmail.com