bf259b986f31ca2d6ff2454e8044b4605e0d8ea0
[fwsnort.git] / install.pl
1 #!/usr/bin/perl -w
2 #
3 #######################################################################
4 #
5 # File: install.pl
6 #
7 # Purpose: This is the installation script for fwsnort.
8 #
9 # Copyright (C) 2003-2011 Michael Rash (mbr@cipherdyne.org)
10 #
11 # License (GNU Public License):
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program; if not, write to the Free Software
20 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
21 #    USA
22 #
23 # TODO:
24 #   - Write the uninstall() routine. :)
25 #
26 #######################################################################
27 #
28
29 use Cwd;
30 use IO::Socket;
31 use File::Copy;
32 use File::Path;
33 use Getopt::Long;
34 use strict;
35
36 #========================= config ========================
37 my $fwsnort_conf_file = 'fwsnort.conf';
38
39 my $sbin_dir    = '/usr/sbin';
40
41 my $update_website = 'www.emergingthreats.net';
42
43 ### system binaries
44 my $perlCmd = '/usr/bin/perl';
45 my $makeCmd = '/usr/bin/make';
46 my $wgetCmd = '/usr/bin/wget';
47 my $gzipCmd = '/bin/gzip';
48 my $tarCmd  = '/bin/tar';
49 #======================= end config ======================
50
51 my %config = ();
52
53 ### map perl modules to versions
54 my %required_perl_modules = (
55     'NetAddr::IP' => {
56         'force-install' => 0,
57         'mod-dir' => 'NetAddr-IP'
58     },
59     'IPTables::Parse' => {
60         'force-install' => 1,
61         'mod-dir' => 'IPTables-Parse'
62     }
63 );
64
65 ### rules update link
66 my $rules_url = 'http://rules.emergingthreats.net/open/snort-2.9.0/emerging-all.rules';
67
68 ### establish some defaults
69 my $uninstall = 0;
70 my $skip_module_install   = 0;
71 my $cmdline_force_install = 0;
72 my $force_mod_re = '';
73 my $exclude_mod_re = '';
74 my $deps_dir = 'deps';
75 my $help = 0;
76 my $locale = 'C';  ### default LC_ALL env variable
77 my $no_locale = 0;
78
79 my $src_dir = getcwd() or die "[*] Could not get current working directory.";
80
81 my %cmds = (
82     'perl' => $perlCmd,
83     'make' => $makeCmd,
84     'gzip' => $gzipCmd,
85     'wget' => $wgetCmd,
86     'tar'  => $tarCmd
87 );
88
89 ### make Getopts case sensitive
90 Getopt::Long::Configure('no_ignore_case');
91
92 &usage(1) unless (GetOptions(
93     'force-mod-install' => \$cmdline_force_install,  ### force install of all modules
94     'Force-mod-regex=s' => \$force_mod_re, ### force specific mod install with regex
95     'Exclude-mod-regex=s' => \$exclude_mod_re, ### exclude a particular perl module
96     'Skip-mod-install'  => \$skip_module_install,
97     'rules-url=s' => \$rules_url,
98     'uninstall' => \$uninstall, ### uninstall fwsnort
99     'LC_ALL=s'  => \$locale,
100     'no-LC_ALL' => \$no_locale,
101     'help'      => \$help
102 ));
103
104 &usage(0) if $help;
105
106 ### set LC_ALL env variable
107 $ENV{'LC_ALL'} = $locale unless $no_locale;
108
109 &import_config();
110
111 $force_mod_re = qr|$force_mod_re| if $force_mod_re;
112 $exclude_mod_re = qr|$exclude_mod_re| if $exclude_mod_re;
113
114 ### see if the deps/ directory exists, and if not then we are installing
115 ### from the -nodeps sources so don't install any perl modules
116 $skip_module_install = 1 unless -d $deps_dir;
117
118 ### make sure the system binaries are where we think they are.
119 &check_commands();
120
121 ### check to make sure we are running as root
122 $< == 0 && $> == 0 or die "You need to be root (or equivalent UID 0",
123     " account) to install/uninstall fwsnort!\n";
124
125 if ($uninstall) {
126     &uninstall();
127 } else {
128     &install()
129 }
130 exit 0;
131 #===================== end main ===================
132
133 sub install() {
134     die "[*] You must run install.pl from the fwsnort " .
135         "sources directory." unless -e 'fwsnort' and -e 'fwsnort.conf';
136
137     unless (-d $config{'CONF_DIR'}) {
138         print "[+] mkdir $config{'CONF_DIR'}\n";
139         mkdir $config{'CONF_DIR'}, 0500;
140     }
141     unless (-d $config{'RULES_DIR'}) {
142         print "[+] mkdir $config{'RULES_DIR'}\n";
143         mkdir $config{'RULES_DIR'}, 0500;
144     }
145
146     ### install perl modules
147     unless ($skip_module_install) {
148         for my $module (keys %required_perl_modules) {
149             &install_perl_module($module);
150         }
151     }
152     chdir $src_dir or die "[*] Could not chdir $src_dir: $!";
153
154     my $local_rules_dir = 'deps/snort_rules';
155     if (-d 'deps' and -d $local_rules_dir
156             and &query_get_emerging_threats_sigs()) {
157         chdir $local_rules_dir or die "[*] Could not chdir $local_rules_dir";
158         if (-e 'emerging-all.rules') {
159             move 'emerging-all.rules', 'emerging-all.rules.tmp'
160                 or die "[*] Could not move emerging-all.rules -> ",
161                 "emerging-all.rules.tmp";
162         }
163         system "$cmds{'wget'} $rules_url";
164         if (-e 'emerging-all.rules') {  ### successful download
165             unlink 'emerging-all.rules.tmp';
166         } else {
167             print "[-] Could not download emerging-all.rules file.\n";
168             if (-e 'emerging-all.rules.tmp') {
169                 ### move the original back
170                 move 'emerging-all.rules', 'emerging-all.rules.tmp'
171                     or die "[*] Could not move emerging-all.rules -> ",
172                     "emerging-all.rules.tmp";
173             }
174         }
175         chdir '../..';
176     }
177
178     if (-d 'deps' and -d $local_rules_dir) {
179         opendir D, $local_rules_dir or die "[*] Could not open ",
180             "the $local_rules_dir directory: $!";
181         my @rfiles = readdir D;
182         closedir D;
183
184         print "[+] Copying all rules files to $config{'RULES_DIR'}\n";
185         for my $rfile (@rfiles) {
186             next unless $rfile =~ /\.rules$/;
187             print "[+] Installing $rfile\n";
188             copy "$local_rules_dir/${rfile}", "$config{'RULES_DIR'}/${rfile}"
189                 or die "[*] Could not copy $local_rules_dir/${rfile} ",
190                     "-> $config{'RULES_DIR'}/${rfile}";
191         }
192     }
193
194     print "\n";
195
196     ### install the fwsnort.8 man page
197     &install_manpage();
198
199     my $preserve_rv = 0;
200     if (-e "$config{'CONF_DIR'}/fwsnort.conf") {
201         $preserve_rv = &query_preserve_config();
202     }
203
204     if ($preserve_rv) {
205         &preserve_config();
206     } else {
207         print "[+] Copying fwsnort.conf -> $config{'CONF_DIR'}/fwsnort.conf\n";
208         copy 'fwsnort.conf', "$config{'CONF_DIR'}/fwsnort.conf";
209         chmod 0600, "$config{'CONF_DIR'}/fwsnort.conf";
210     }
211
212     print "[+] Copying fwsnort -> ${sbin_dir}/fwsnort\n";
213     copy 'fwsnort', "${sbin_dir}/fwsnort";
214     chmod 0500, "${sbin_dir}/fwsnort";
215
216     print "\n========================================================\n",
217         "\n[+] fwsnort will generate an iptables script located at:\n",
218         "    /var/lib/fwsnort.sh when executed.\n",
219         "\n[+] fwsnort has been successfully installed!\n\n";
220
221     return;
222 }
223
224 sub install_perl_module() {
225     my $mod_name = shift;
226
227     chdir $src_dir or die "[*] Could not chdir $src_dir: $!";
228     chdir $deps_dir or die "[*] Could not chdir($deps_dir): $!";
229
230     die '[*] Missing force-install key in required_perl_modules hash.'
231         unless defined $required_perl_modules{$mod_name}{'force-install'};
232     die '[*] Missing mod-dir key in required_perl_modules hash.'
233         unless defined $required_perl_modules{$mod_name}{'mod-dir'};
234
235     if ($exclude_mod_re and $exclude_mod_re =~ /$mod_name/) {
236         print "[+] Excluding installation of $mod_name module.\n";
237         return;
238     }
239
240     my $version = '(NA)';
241
242     my $mod_dir = $required_perl_modules{$mod_name}{'mod-dir'};
243
244     if (-e "$mod_dir/VERSION") {
245         open F, "< $mod_dir/VERSION" or
246             die "[*] Could not open $mod_dir/VERSION: $!";
247         $version = <F>;
248         close F;
249         chomp $version;
250     } else {
251         print "[-] Warning: VERSION file does not exist in $mod_dir\n";
252     }
253
254     my $install_module = 0;
255
256     if ($required_perl_modules{$mod_name}{'force-install'}
257             or $cmdline_force_install) {
258         ### install regardless of whether the module may already be
259         ### installed
260         $install_module = 1;
261     } elsif ($force_mod_re and $force_mod_re =~ /$mod_name/) {
262         print "[+] Forcing installation of $mod_name module.\n";
263         $install_module = 1;
264     } else {
265         if (has_perl_module($mod_name)) {
266             print "[+] Module $mod_name is already installed in the ",
267                 "system perl tree, skipping.\n";
268         } else {
269             ### install the module in the /usr/lib/fwsnort directory because
270             ### it is not already installed.
271             $install_module = 1;
272         }
273     }
274
275     if ($install_module) {
276         unless (-d $config{'LIBS_DIR'}) {
277             print "[+] Creating $config{'LIBS_DIR'}\n";
278             mkdir $config{'LIBS_DIR'}, 0755
279                 or die "[*] Could not mkdir $config{'LIBS_DIR'}: $!";
280         }
281         print "[+] Installing the $mod_name $version perl " .
282             "module in $config{'LIBS_DIR'}/\n";
283         my $mod_dir = $required_perl_modules{$mod_name}{'mod-dir'};
284         chdir $mod_dir or die "[*] Could not chdir to ",
285             "$mod_dir: $!";
286         unless (-e 'Makefile.PL') {
287             die "[*] Your $mod_name source directory appears to be incomplete!\n",
288                 "    Download the latest sources from ",
289                 "http://www.cipherdyne.org/\n";
290         }
291         system "$cmds{'make'} clean" if -e 'Makefile';
292         system "$cmds{'perl'} Makefile.PL " .
293             "PREFIX=$config{'LIBS_DIR'} LIB=$config{'LIBS_DIR'}";
294         system $cmds{'make'};
295 #        system "$cmds{'make'} test";
296         system "$cmds{'make'} install";
297
298         print "\n\n";
299     }
300     chdir $src_dir or die "[*] Could not chdir $src_dir: $!";
301     return;
302 }
303
304 sub has_perl_module() {
305     my $module = shift;
306
307     # 5.8.0 has a bug with require Foo::Bar alone in an eval, so an
308     # extra statement is a workaround.
309     my $file = "$module.pm";
310     $file =~ s{::}{/}g;
311     eval { require $file };
312
313     return $@ ? 0 : 1;
314 }
315
316 sub uninstall() {
317     ### FIXME
318     return;
319 }
320
321 sub install_manpage() {
322     my $manpage = 'fwsnort.8';
323     ### remove old man page
324     unlink "/usr/local/man/man8/${manpage}" if
325         (-e "/usr/local/man/man8/${manpage}");
326
327     ### default location to put the fwsnort man page, but check with
328     ### /etc/man.config
329     my $mpath = '/usr/share/man/man8';
330     if (-e '/etc/man.config') {
331         ### prefer to install $manpage in /usr/local/man/man8 if
332         ### this directory is configured in /etc/man.config
333         open M, '< /etc/man.config' or
334             die "[*] Could not open /etc/man.config: $!";
335         my @lines = <M>;
336         close M;
337         ### prefer the path "/usr/share/man"
338         my $found = 0;
339         for my $line (@lines) {
340             chomp $line;
341             if ($line =~ m|^MANPATH\s+/usr/share/man|) {
342                 $found = 1;
343                 last;
344             }
345         }
346         ### try to find "/usr/local/man" if we didn't find /usr/share/man
347         unless ($found) {
348             for my $line (@lines) {
349                 chomp $line;
350                 if ($line =~ m|^MANPATH\s+/usr/local/man|) {
351                     $mpath = '/usr/local/man/man8';
352                     $found = 1;
353                     last;
354                 }
355             }
356         }
357         ### if we still have not found one of the above man paths,
358         ### just select the first one out of /etc/man.config
359         unless ($found) {
360             for my $line (@lines) {
361                 chomp $line;
362                 if ($line =~ m|^MANPATH\s+(\S+)|) {
363                     $mpath = $1;
364                     last;
365                 }
366             }
367         }
368     }
369     mkdir $mpath, 0755 unless -d $mpath;
370     my $mfile = "${mpath}/${manpage}";
371     print "[+] Installing $manpage man page as $mfile\n";
372     copy $manpage, $mfile or die "[*] Could not copy $manpage to " .
373         "$mfile: $!";
374     chmod 0644, $mfile;
375     print "[+] Compressing manpage $mfile\n";
376     ### remove the old one so gzip doesn't prompt us
377     unlink "${mfile}.gz" if -e "${mfile}.gz";
378     system "$gzipCmd $mfile";
379     return;
380 }
381
382 sub query_get_emerging_threats_sigs() {
383     my $ans = '';
384     print "[+] Would you like to download the latest Snort rules from \n",
385         "    http://$update_website/?\n";
386     while ($ans ne 'y' && $ans ne 'n') {
387         print "    ([y]/n)?  ";
388         $ans = <STDIN>;
389         return 1 if $ans eq "\n";
390         chomp $ans;
391     }
392     if ($ans eq 'y') {
393         return 1;
394     }
395     return 0;
396 }
397
398 sub import_config() {
399     open C, "< $fwsnort_conf_file"
400         or die "[*] Could not open $fwsnort_conf_file: $!";
401     while (<C>) {
402         next if /^\s*#/;
403         if (/^\s*(\S+)\s+(.*?)\;/) {
404             my $varname = $1;
405             my $val     = $2;
406             if ($val =~ m|/.+| and $varname =~ /^\s*(\S+)Cmd$/) {
407                 ### found a command
408                 $cmds{$1} = $val;
409             } else {
410                 $config{$varname} = $val;
411             }
412         }
413     }
414     close C;
415
416     ### resolve internal vars within variable values
417     &expand_vars();
418
419     &required_vars();
420
421     return;
422 }
423
424 sub expand_vars() {
425
426     my $has_sub_var = 1;
427     my $resolve_ctr = 0;
428
429     while ($has_sub_var) {
430         $resolve_ctr++;
431         $has_sub_var = 0;
432         if ($resolve_ctr >= 20) {
433             die "[*] Exceeded maximum variable resolution counter.";
434         }
435         for my $hr (\%config, \%cmds) {
436             for my $var (keys %$hr) {
437                 my $val = $hr->{$var};
438                 if ($val =~ m|\$(\w+)|) {
439                     my $sub_var = $1;
440                     die "[*] sub-ver $sub_var not allowed within same ",
441                         "variable $var" if $sub_var eq $var;
442                     if (defined $config{$sub_var}) {
443                         $val =~ s|\$$sub_var|$config{$sub_var}|;
444                         $hr->{$var} = $val;
445                     } else {
446                         die "[*] sub-var \"$sub_var\" not defined in ",
447                             "config for var: $var."
448                     }
449                     $has_sub_var = 1;
450                 }
451             }
452         }
453     }
454     return;
455 }
456
457 sub required_vars() {
458     my @required_vars = qw(
459         CONF_DIR RULES_DIR ARCHIVE_DIR QUEUE_RULES_DIR LOG_DIR LIBS_DIR
460         CONF_FILE FWSNORT_SCRIPT LOG_FILE
461     );
462     for my $var (@required_vars) {
463         die "[*] Variable $var not defined in $fwsnort_conf_file. Exiting.\n"
464             unless defined $config{$var};
465     }
466     return;
467 }
468
469 sub check_commands() {
470     my @path = qw(
471         /bin
472         /usr/bin
473         /usr/local/bin
474     );
475     CMD: for my $cmd (keys %cmds) {
476         unless (-x $cmds{$cmd}) {
477             my $found = 0;
478             PATH: for my $dir (@path) {
479                 if (-x "${dir}/${cmd}") {
480                     $cmds{$cmd} = "${dir}/${cmd}";
481                     $found = 1;
482                     last PATH;
483                 }
484             }
485             unless ($found) {
486                 die "[*] Could not find $cmd, edit the ",
487                     "config section of install.pl";
488             }
489         }
490     }
491
492     return;
493 }
494
495 sub query_preserve_config() {
496     my $ans = '';
497     while ($ans ne 'y' && $ans ne 'n') {
498         print "[+] Would you like to preserve the config from the\n",
499             '    existing fwsnort installation ([y]/n)?  ';
500         $ans = <STDIN>;
501         return 1 if $ans eq "\n";
502         chomp $ans;
503     }
504     if ($ans eq 'y') {
505         return 1;
506     }
507     return 0;
508 }
509
510 sub preserve_config() {
511     my $file = 'fwsnort.conf';
512     open C, "< $file" or die "[*] Could not open $file: $!";
513     my @new_lines = <C>;
514     close C;
515
516     open CO, "< $config{'CONF_DIR'}/$file" or die "[*] Could not open ",
517         "$config{'CONF_DIR'}/$file: $!";
518     my @orig_lines = <CO>;
519     close CO;
520
521     print "[+] Preserving existing config: $config{'CONF_DIR'}/$file\n";
522     ### write to a tmp file and then move.
523     my $printed_intf_warning = 0;
524     open CONF, "> $config{'CONF_DIR'}/${file}.new" or die "[*] Could not open ",
525         "$config{'CONF_DIR'}/${file}.new: $!";
526     for my $new_line (@new_lines) {
527         if ($new_line =~ /^\s*#/) {
528             print CONF $new_line;
529         } elsif ($new_line =~ /^\s*(\S+)/) {
530             my $var = $1;
531             my $found = 0;
532             for my $orig_line (@orig_lines) {
533                 if ($orig_line =~ /^\s*\S+INTF\s/) {
534                     ### interfaces are no longer used!
535                     unless ($printed_intf_warning) {
536                         print "    NOTE: Interfaces are no longer used as of the ",
537                         "0.8.0 release;\n    removing $var\n";
538                         $printed_intf_warning = 1;
539                     }
540                 }
541                 if ($orig_line =~ /^\s*$var\s/
542                         and $orig_line !~ /INTF/) {
543                     print CONF $orig_line;
544                     $found = 1;
545                     last;
546                 }
547             }
548             unless ($found) {
549                 print CONF $new_line;
550             }
551         } else {
552             print CONF $new_line;
553         }
554     }
555     close CONF;
556     move "$config{'CONF_DIR'}/${file}.new", "$config{'CONF_DIR'}/$file";
557     return;
558 }
559
560 sub usage() {
561     my $exit = shift;
562     print <<_HELP_;
563 install.pl: [options]
564     -f, --force-mod-install        - Install all perl modules regardless
565                                      of whether they already installed on
566                                      the system.
567     -F, --Force-mod-regex <regex>  - Install perl module that matches a
568                                      specific regular expression.
569     -E, --Exclude-mod-regex <re>   - Exclude a perl module that matches this
570                                      regular expression.
571     -S, --Skip-mod-install         - Skip installation of modules.
572     -r, --rules-url <url>          - Specify the URL to use for updating the
573                                      Emerging Threats rule set - the default is:
574                                      $rules_url
575     -u, --uninstall   - uninstall fwsnort
576     -h, --help        - print help and exit
577 _HELP_
578     exit $exit;
579 }