new upstream release (3.3.0); modify package compatibility for Stretch
[ossec-hids.git] / contrib / ossec-batch-manager.pl
1 #!/usr/bin/perl
2 # vim:shiftwidth=2:tabstop=2:expandtab:textwidth=80:softtabstop=2:ai:
3
4    #########################################################
5   # Written Aug 4, 2007 and released under the GNU/GPLv2  ##
6  # by Jeff Schroeder (jeffschroeder@computer.org)        # #
7 #########################################################  #
8 #                                                       #  #
9 # ossec-batch-manager.pl - Add and extract agents from  #  #
10 # the ossec client.keys file non-interactively. This    #  #
11 # started as a hack to properly script manage_agents.   #  #
12 #                                                       # #
13 ##########################################################
14 # Modified by Tim Meader (Timothy.A.Meader@nasa.gov)
15 # on 2013/07/01
16 #
17 # - corrected a MAJOR logic error in the remove
18 #   function. The comparison was being done across the
19 #   entire line of the agent keys file, so both IPs
20 #   and the SSH keys at the end could be matched against
21 #   the 'agent ID' wanting to be removed. Changed the
22 #   match to only compare the first column of the file
23 # - added an error output message to the remove
24 #   function if it's fed an 'agent ID' that doesn't
25 #   exist
26 # - the script now also removes the corresponding
27 #   associated agent rid files after a successful remove
28 #   operation, or gives an error on failure
29 #            
30 ##########################################################
31 # Modified by Tim Meader (Timothy.A.Meader@nasa.gov)
32 # on 2010/12/08
33 #            
34 # - fixed two errors that were popping up during add or
35 #   remove operations due to the code not taking into
36 #   account the old key entries that have the "#*#*#*"
37 #   pattern after the ID number. Simple fix was to do
38 #   a "if (defined(xxx))" on the vars
39 # - fixed the "list" operation to only show valid key
40 #   entries
41 # - changed the extract operation to store options
42 #   in an array, and subsequently rewrote the 
43 #   "extract_key" (now called "extract_keys") func
44 #   to accept this new behavior
45 # - modified "extract_keys" func to accept either ID,
46 #   name, or IP address as the argument after the
47 #   "-e" operator. Output of key extraction now
48 #   include the name and IP address by default in the
49 #   format: "name,IP  extracted_key"
50 #
51 #########################################################
52
53
54 #$Id$
55 # TODO:
56 #   - Add check for ossec 1.4 and support longer agent names
57 #   - Add in eval so that older version of perl without
58 #     Time::HiRes still can use this script.
59
60 use strict;
61 use warnings;
62 require 5.8.2; # Time::HiRes is standard from this version forth
63 #use diagnostics;
64 use MIME::Base64;
65 use Digest::MD5 qw(md5_hex);
66 use Getopt::Long;
67 use Regexp::Common::net;
68
69 use constant AUTH_KEY_FILE => "/var/ossec/etc/client.keys";
70 use constant RIDS_PATH => "/var/ossec/queue/rids/";
71
72 my ($key, $add, $remove, @extracts, $import, $listagents);
73 my ($agentid, $agentname, $ipaddress);
74
75 GetOptions(
76   'k|key=s'     => \$key,         # Unencoded ssh key
77   'a|add'       => \$add,         # Add a new agent
78   'r|remove=s'  => \$remove,      # Remove an agent
79   'e|extract=s' => \@extracts,     # Extract a key
80   'm|import'    => \$import,      # Import a key
81   'l|list'      => \$listagents,  # List all agents
82   'i|id=s'      => \$agentid,     # Unique agent id
83   'n|name=s'    => \$agentname,   # Agent name. 32 char max
84   'p|ip=s'      => \$ipaddress    # IP Address in "dotted quad" notation
85 );
86
87 # Spit out a list of available agents, their names, and ip information
88 if ($listagents) {
89   list_agents();
90 }
91 # Decode and extract the key for $agentid
92 elsif (@extracts) {
93   if (@extracts) {
94     extract_keys(@extracts);
95   }
96   else {
97     usage();
98   }
99 }
100 # Adding a new agent
101 elsif ($add) {
102   if ($agentname && $ipaddress && 
103       (
104           $ipaddress =~ m/$RE{net}{IPv4}/
105               ||
106           $ipaddress =~ m/$RE{net}{IPv6}/
107               ||
108           $ipaddress eq 'any'
109       ) &&
110       # ossec doesn't like agent names > 32 characters.
111       length($agentname) <= 32) {
112
113       # Autogenerate an id incremented 1 from the last in a sorted list of
114       # all current ones if it isn't specified from the command line.
115       if (!$agentid) {
116
117         # Make a list of all of the used agentids and then sort it.
118         if (-r AUTH_KEY_FILE) {
119           my @used_agent_ids = ();
120           open (FH, "<", AUTH_KEY_FILE);
121           while (<FH>) {
122             my ($id, $name, $ip, $key) = split;
123             push(@used_agent_ids, $id);
124           }
125           close(FH);
126
127           if (@used_agent_ids) {
128             @used_agent_ids = sort {$a <=> $b} @used_agent_ids;
129             $agentid = sprintf("%03d", $used_agent_ids[-1] + 1);
130           }
131         }
132         # If the client.keys is empty or doesn't exist set the id to 001
133         $agentid = sprintf("%03d", 001) if (!$agentid);
134         }
135
136     # Autogenerate a key unless one was specified on the command line
137     if (!$key) {
138       use Time::HiRes; # Standard with perl >= 5.8.2
139
140       my $rand_str1 = time() . $agentname . rand(10000);
141       my $rand_str2 = Time::HiRes::time . $ipaddress . $agentid . rand(10000);
142       $key = md5_hex($rand_str1) . md5_hex($rand_str2);
143     }
144       
145     add_agent($agentid, $agentname, $ipaddress, $key);
146   }
147   else {
148     warn "Error: adding agents requires: --name and --ip options.\n";
149     usage();
150   }
151 }
152 elsif ($remove) {
153   if ($agentid) {
154     remove_agent($agentid);
155   }
156   else {
157     remove_agent($remove)
158   }
159 }
160 elsif ($import) {
161   # Every option needs to be specified and NOT autogenerated because what
162   # is autogenerated on the server and the agent will likely be different
163   if (!$agentid || !$agentname || !$ipaddress || !$key) {
164     warn "Error: importing requires: --id, --name, --ip, and --key\n";
165     usage();
166   }
167   else {
168     # The key extracted from the server needs to be decoded before being put
169     # into the client.keys 
170     $key = MIME::Base64::decode($key);
171
172     add_agent($agentid, $agentname, $ipaddress, $key);
173   }
174 }
175 else {
176   warn "Error: no options specified!\n";
177   usage();
178 }
179
180 sub usage {
181   warn "Usage: $0 [OPERATION] [OPTIONS]\n";
182   warn "  [operations]\n";
183   warn "    -a or --add                    = Add a new agent\n";
184   warn "    -r or --remove  [id]           = Remove agent\n";
185   warn "    -e or --extract [id|name|ip]   = Extract key\n";
186   warn "    -m or --import  [keydata]      = Import key\n";
187   warn "    -l or --list                   = List available agents\n";
188   warn "  [options]\n";
189   warn "    -k or --key     [keydata]  = Key data\n";
190   warn "    -n or --name    [name]     = Agent name (32 character max)\n";
191   warn "    -i or --id      [id]       = Agent identification (integer)\n";
192   warn "    -p or --ip      [ip]       = IP address\n\n";
193   exit 1;
194 }
195
196 sub list_agents {
197   if (-r AUTH_KEY_FILE) {
198     open (FH, "<", AUTH_KEY_FILE);
199   }
200   else {
201     die "Error reading ".AUTH_KEY_FILE.": $!\n";
202   }
203   print "Available Agents:\n";
204   print "ID",     " " x (25 - length('ID')),
205         "NAME",   " " x (25 - length('NAME')),
206         "IP",     " " x (25 - length('IP'));
207   print "\n";
208   while (<FH>) {
209     chomp;
210     my ($id, $name, $ip, $key) = split;
211     if (defined($key)) {
212       print "$id",    " " x (25 - length($id)),
213             "$name",  " " x (25 - length($name)),
214             "$ip",    " " x (25 - length($ip)) . "\n";
215     }
216   }
217   close(FH);
218   exit 0;
219 }
220
221 sub extract_keys {
222   if (-r AUTH_KEY_FILE) {
223     open (FH, "<", AUTH_KEY_FILE);
224   }
225   else {
226     die "No ".AUTH_KEY_FILE."!\n";
227   }
228   
229   foreach my $extract (@_) {
230     my ($encoded, $decoded);
231     my $found = 0;
232
233     while (<FH>) {
234       chomp;
235       my ($id, $name, $ip, $key) = split;
236       # Check to make sure it's a valid entry
237       if (defined($key)) {
238         if (($extract =~ /^\d+$/) && ($id == $extract)) {
239           $found = 1;
240         }
241         elsif ($name eq $extract) {
242           $found = 1;
243         }
244         elsif ($ip eq $extract) {
245           $found = 1;
246         }
247         else {
248           next;
249         }
250         # Newlines are valid base64 characters so use '' instead for \n
251         $decoded = MIME::Base64::encode($_, '');
252         print "$name,$ip  $decoded\n";
253         next;
254       }
255     }
256     if (!$found) {
257       warn "Error: Agent $extract doesn't exist!\n";
258     }
259     seek FH,0,0;
260   }
261 }
262
263 sub add_agent {
264   my $id = shift;
265   my $name = shift;
266   my $ip = shift;
267   my $agentkey = shift;
268
269   if ($name && $ip && $agentkey) {
270     # Valid example key:
271     # 5a832efb8f93660857ce2acf8eec66a19fd9d4fa58e3221bbd2927ca8a0b40c3
272     if ($agentkey !~ m/[a-z0-9]{64}/) { 
273       warn "Error: invalid keydata! Let this script autogenerate it.\n";
274       usage();
275     }
276
277     my @newagent = ($id, $name, $ip, $agentkey);
278     my $exists = check_if_exists(\@newagent);
279
280     if ($exists == 0) {
281       # Append if client.keys exists and create it if it doesn't
282       if (-e AUTH_KEY_FILE) {
283         open(FH, ">>", AUTH_KEY_FILE) or die AUTH_KEY_FILE." error: $!\n";
284       }
285       else {
286         open(FH, ">", AUTH_KEY_FILE) or die AUTH_KEY_FILE." error: $!\n";
287       }
288       print FH join(' ', @newagent), "\n";
289       close(FH);
290     }
291     elsif ($exists == 1) {
292       warn "ID: $id already in ".AUTH_KEY_FILE."!\n";
293     }
294     elsif ($exists == 2) {
295       warn "Agent: $name already in ".AUTH_KEY_FILE."!\n";
296     }
297     elsif ($exists == 3) {
298       warn "IP: $ip already in ".AUTH_KEY_FILE."!\n";
299     }
300   }
301   else {
302     warn "Missing options to --add or problem with ".AUTH_KEY_FILE.": $!\n";
303     usage();
304   }
305 }
306
307 sub remove_agent {
308   my $removeid = shift;
309   my @agent_array;
310
311   if (-r AUTH_KEY_FILE) {
312     open (FH, "<", AUTH_KEY_FILE);
313   }
314   else {
315     die "Error: with ".AUTH_KEY_FILE.": $!\n";
316   }
317   while (<FH>) {
318     push(@agent_array, $_);
319   }
320   close(FH);
321
322   if (-w AUTH_KEY_FILE) {
323     open (FHRW, ">", AUTH_KEY_FILE);
324   }
325   else {
326     die "Error writing ".AUTH_KEY_FILE.": $!\n";
327   }
328
329   my $key_found = 0;
330
331   foreach my $line (@agent_array) {
332     my @split_line = split(/\s/,$line);
333
334     if ($split_line[0] ne $removeid) {
335       print FHRW "$line";
336     }
337     else {
338       my $rids_file = RIDS_PATH.$removeid;
339       $key_found = 1;
340       unlink $rids_file or warn "Could not remove rids file for Agent ID \'".$removeid."\'!\n";
341     }
342   }
343   close(FHRW);
344
345   if (!$key_found) {
346     die "Agent ID \'".$removeid."\' not found! Nothing removed.\n";
347   }
348   exit(0);
349 }
350
351 sub check_if_exists {
352   my $agentlist_ref = shift;
353   my ($newid, $newname, $newip);
354   my $rval = 0;
355
356   $newid = $agentlist_ref->[0];
357   $newname = $agentlist_ref->[1];
358   $newip = $agentlist_ref->[2];
359
360   # If the file isn't readable, the id probably isn't already in it
361   if (-r AUTH_KEY_FILE) {
362     open (FH, "<", AUTH_KEY_FILE);
363     while (<FH>) {
364       chomp;
365       my ($id, $name, $ip, $key) = split;
366       if(defined($key)) {
367         $rval = 1 if ($id == $newid && $rval == 0);
368         $rval = 2 if ($name eq $newname && $rval == 0); 
369         $rval = 3 if ($ip ne 'any' && $ip eq $newip && $rval == 0);
370       }
371     }
372     close(FH);
373   }
374   return $rval;
375 }
376