PageRenderTime 70ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/modsecurity-apache_2.6.1/tools/rules-updater.pl.in

http://vulture.googlecode.com/
Autoconf | 480 lines | 368 code | 72 blank | 40 comment | 67 complexity | 2963fb9ee3caf17c2f88732538c8e08e MD5 | raw file
Possible License(s): Apache-2.0
  1. #!@PERL@
  2. #
  3. # Fetches the latest ModSecurity Ruleset
  4. #
  5. use strict;
  6. use Sys::Hostname;
  7. use LWP::UserAgent ();
  8. use LWP::Debug qw(-);
  9. use URI ();
  10. use HTTP::Date ();
  11. use Cwd qw(getcwd);
  12. use Getopt::Std;
  13. my $VERSION = "0.0.1";
  14. my($SCRIPT) = ($0 =~ m/([^\/\\]+)$/);
  15. my $CRLFRE = qr/\015?\012/;
  16. my $HOST = Sys::Hostname::hostname();
  17. my $UNZIP = [qw(unzip -a)];
  18. my $SENDMAIL = [qw(/usr/lib/sendmail -oi -t)];
  19. my $HAVE_GNUPG = 0;
  20. my %PREFIX_MAP = (
  21. -dev => 0,
  22. -rc => 1,
  23. "" => 9,
  24. );
  25. my %GPG_TRUST = ();
  26. my $REQUIRED_SIG_TRUST;
  27. eval "use GnuPG qw(:trust)";
  28. if ($@) {
  29. warn "Could not load GnuPG module - cannot verify ruleset signatures\n";
  30. }
  31. else {
  32. $HAVE_GNUPG = 1;
  33. %GPG_TRUST = (
  34. &TRUST_UNDEFINED => "not",
  35. &TRUST_NEVER => "not",
  36. &TRUST_MARGINAL => "marginally",
  37. &TRUST_FULLY => "fully",
  38. &TRUST_ULTIMATE => "ultimatly",
  39. );
  40. $REQUIRED_SIG_TRUST = &TRUST_FULLY;
  41. }
  42. ################################################################################
  43. ################################################################################
  44. my @fetched = ();
  45. my %opt = ();
  46. getopts('c:r:p:s:v:t:e:f:EuS:D:R:U:F:L:ldh', \%opt);
  47. usage(1) if(defined $opt{h});
  48. usage(1) if(@ARGV > 1);
  49. # Make sure we have an action
  50. if (! grep { defined } @opt{qw(S D R U F L l)}) {
  51. usage(1, "Action required.");
  52. }
  53. # Merge config with commandline opts
  54. if ($opt{c}) {
  55. %opt = parse_config($opt{c}, \%opt);
  56. }
  57. LWP::Debug::level("+") if ($opt{d});
  58. # Make the version into a regex
  59. if (defined $opt{v}) {
  60. my($a,$b,$c,$d) = ($opt{v} =~ m/^(\d+)\.?(\d+)?\.?(\d+)?(?:-(\D+\d+$)|($))/);
  61. if (defined $d) {
  62. (my $key = $d) =~ s/^(\D+)\d+$/-$1/;
  63. unless (exists $PREFIX_MAP{$key}) {
  64. usage(1, "Invalid version (bad suffix \"$d\"): $opt{v}");
  65. }
  66. $opt{v} = qr/^$a\.$b\.$c-$d$/;
  67. }
  68. elsif (defined $c) {
  69. $opt{v} = qr/^$a\.$b\.$c(?:-|$)/;
  70. }
  71. elsif (defined $b) {
  72. $opt{v} = qr/^$a\.$b\./;
  73. }
  74. elsif (defined $a) {
  75. $opt{v} = qr/^$a\./;
  76. }
  77. else {
  78. usage(1, "Invalid version: $opt{v}");
  79. }
  80. if ($opt{d}) {
  81. print STDERR "Using version regex: $opt{v}\n";
  82. }
  83. }
  84. else {
  85. $opt{v} = qr/^/;
  86. }
  87. # Remove trailing slashes from uri and path
  88. $opt{r} =~ s/\/+$//;
  89. $opt{p} =~ s/\/+$//;
  90. # Required opts
  91. usage(1, "Repository (-r) required.") unless(defined $opt{r});
  92. usage(1, "Local path (-p) required.") unless(defined $opt{p} or defined $opt{l});
  93. my $ua = LWP::UserAgent->new(
  94. agent => "ModSecurity Updator/$VERSION",
  95. keep_alive => 1,
  96. env_proxy => 1,
  97. max_redirect => 5,
  98. requests_redirectable => [qw(GET HEAD)],
  99. timeout => ($opt{t} || 600),
  100. );
  101. sub usage {
  102. my $rc = defined($$_[0]) ? $_[0] : 0;
  103. my $msg = defined($_[1]) ? "\n$_[1]\n\n" : "";
  104. print STDERR << "EOT";
  105. ${msg}Usage: $SCRIPT [-c config_file] [[options] [action]
  106. Options (commandline will override config file):
  107. -r uri RepositoryURI Repository URI.
  108. -p path LocalRepository Local repository path to use as base for downloads.
  109. -s path LocalRules Local rules base path to use for unpacking.
  110. -v text Version Full/partial version (EX: 1, 1.5, 1.5.2, 1.5.2-dev3)
  111. -t secs Timeout Timeout for fetching data in seconds (default 600).
  112. -e addr NotifyEmail Notify via email on update (comma separated list).
  113. -f addr NotifyEmailFrom From address for notification email.
  114. -u Unpack Unpack into LocalRules/version path.
  115. -d Debug Print out lots of debugging.
  116. Actions:
  117. -S name Fetch the latest stable ruleset, "name"
  118. -D name Fetch the latest development ruleset, "name"
  119. -R name Fetch the latest release candidate ruleset, "name"
  120. -U name Fetch the latest unstable (non-stable) ruleset, "name"
  121. -F name Fetch the latest ruleset, "name"
  122. -l Print listing of what is available
  123. Misc:
  124. -c Specify a config file for options.
  125. -h This help
  126. Examples:
  127. # Get a list of what the repository contains:
  128. $SCRIPT -rhttp://host/repo/ -l
  129. # Get a partial list of versions 1.5.x:
  130. $SCRIPT -rhttp://host/repo/ -v1.5 -l
  131. # Get the latest stable version of "breach_ModSecurityCoreRules":
  132. $SCRIPT -rhttp://host/repo/ -p/my/repo -Sbreach_ModSecurityCoreRules
  133. # Get the latest stable 1.5 release of "breach_ModSecurityCoreRules":
  134. $SCRIPT -rhttp://host/repo/ -p/my/repo -v1.5 -Sbreach_ModSecurityCoreRules
  135. EOT
  136. exit $rc;
  137. }
  138. sub sort_versions {
  139. (my $A = $a) =~ s/^(\d+)\.(\d+)\.(\d+)(-[^-\d]+|)(\d*)$/sprintf("%03d%03d%03d%03d%03d", $1, $2, $3, $PREFIX_MAP{$4}, $5)/e;
  140. (my $B = $b) =~ s/^(\d+)\.(\d+)\.(\d+)(-[^-\d]+|)(\d*)$/sprintf("%03d%03d%03d%03d%03d", $1, $2, $3, $PREFIX_MAP{$4}, $5)/e;
  141. return $A cmp $B;
  142. }
  143. sub parse_config {
  144. my($file,$clo) = @_;
  145. my %cfg = ();
  146. print STDERR "Parsing config: $file\n" if ($opt{d});
  147. open(CFG, "<$file") or die "Failed to open config \"$file\": $!\n";
  148. while(<CFG>) {
  149. # Skip comments and empty lines
  150. next if (/^\s*(?:#|$)/);
  151. # Parse
  152. chomp;
  153. my($var,$q1,$val,$q2) = (m/^\s*(\S+)\s+(['"]?)(.*)(\2)\s*$/);
  154. # Fixup values
  155. $var = lc($var);
  156. if ($val =~ m/^(?:true|on)$/i) { $val = 1 };
  157. if ($val =~ m/^(?:false|off)$/i) { $val = 0 };
  158. # Set opts
  159. if ($var eq "repositoryuri") { $cfg{r} = $val }
  160. elsif ($var eq "localrepository") { $cfg{p} = $val }
  161. elsif ($var eq "localrules") { $cfg{s} = $val }
  162. elsif ($var eq "version") { $cfg{v} = $val }
  163. elsif ($var eq "timeout") { $cfg{t} = $val }
  164. elsif ($var eq "notifyemail") { $cfg{e} = $val }
  165. elsif ($var eq "notifyemailfrom") { $cfg{f} = $val }
  166. elsif ($var eq "notifyemaildiff") { $cfg{E} = $val }
  167. elsif ($var eq "unpack") { $cfg{u} = $val }
  168. elsif ($var eq "debug") { $cfg{d} = $val }
  169. else { die "Invalid config directive: $var\n" }
  170. }
  171. close CFG;
  172. my($k, $v);
  173. while (($k, $v) = each %{$clo || {}}) {
  174. $cfg{$k} = $v if (defined $v);
  175. }
  176. return %cfg;
  177. }
  178. sub repository_dump {
  179. my @replist = repository_listing();
  180. print STDERR "\nRepository: $opt{r}\n\n";
  181. unless (@replist) {
  182. print STDERR "No matching entries.\n";
  183. return;
  184. }
  185. for my $repo (@replist) {
  186. print "$repo {\n";
  187. my @versions = ruleset_available_versions($repo);
  188. for my $version (@versions) {
  189. if ($version =~ m/$opt{v}/) {
  190. printf "%15s: %s_%s.zip\n", $version, $repo, $version;
  191. }
  192. elsif ($opt{d}) {
  193. print STDERR "Skipping version: $version\n";
  194. }
  195. }
  196. print "}\n";
  197. }
  198. }
  199. sub repository_listing {
  200. my $res = $ua->get("$opt{r}/.listing");
  201. unless ($res->is_success()) {
  202. die "Failed to get repository listing \"$opt{r}/.listing\": ".$res->status_line()."\n";
  203. }
  204. return grep(/\S/, split(/$CRLFRE/, $res->content)) ;
  205. }
  206. sub ruleset_listing {
  207. my $res = $ua->get("$opt{r}/$_[0]/.listing");
  208. unless ($res->is_success()) {
  209. die "Failed to get ruleset listing \"$opt{r}/$_[0]/.listing\": ".$res->status_line()."\n";
  210. }
  211. return grep(/\S/, split(/$CRLFRE/, $res->content)) ;
  212. }
  213. sub ruleset_available_versions {
  214. return sort sort_versions map { m/_([^_]+)\.zip.*$/; $1 } ruleset_listing($_[0]);
  215. }
  216. sub ruleset_fetch {
  217. my($repo, $version) = @_;
  218. # Create paths
  219. if (! -e "$opt{p}" ) {
  220. mkdir "$opt{p}" or die "Failed to create \"$opt{p}\": $!\n";
  221. }
  222. if (! -e "$opt{p}/$repo" ) {
  223. mkdir "$opt{p}/$repo" or die "Failed to create \"$opt{p}/$repo\": $!\n";
  224. }
  225. my $fn = "${repo}_$version.zip";
  226. my $ruleset = "$repo/$fn";
  227. my $ruleset_sig = "$repo/$fn.sig";
  228. if (-e "$opt{p}/$ruleset") {
  229. die "Refused to overwrite ruleset \"$opt{p}/$ruleset\".\n";
  230. }
  231. # Fetch the ruleset
  232. print STDERR "Fetching: $ruleset ...\n";
  233. my $res = $ua->get(
  234. "$opt{r}/$ruleset",
  235. ":content_file" => "$opt{p}/$ruleset",
  236. );
  237. die "Failed to retrieve ruleset $ruleset: ".$res->status_line()."\n" unless ($res->is_success());
  238. # Fetch the ruleset signature
  239. if (-e "$opt{p}/$ruleset_sig") {
  240. die "Refused to overwrite ruleset signature \"$opt{p}/$ruleset_sig\".\n";
  241. }
  242. $res = $ua->get(
  243. "$opt{r}/$ruleset_sig",
  244. ":content_file" => "$opt{p}/$ruleset_sig",
  245. );
  246. # Verify the signature if we can
  247. if ($HAVE_GNUPG) {
  248. die "Failed to retrieve ruleset signature $ruleset_sig: ".$res->status_line()."\n" unless ($res->is_success());
  249. ruleset_verifysig("$opt{p}/$ruleset", "$opt{p}/$ruleset_sig");
  250. }
  251. push @fetched, [$repo, $version, $ruleset, undef];
  252. }
  253. sub ruleset_unpack {
  254. my($repo, $version, $ruleset) = @{ $_[0] || [] };
  255. my $fn = "$opt{p}/$ruleset";
  256. if (! -e "$fn" ) {
  257. die "Internal Error: No ruleset to unpack - \"$fn\"\n";
  258. }
  259. # Create paths
  260. if (! -e "$opt{s}" ) {
  261. mkdir "$opt{s}" or die "Failed to create \"$opt{p}\": $!\n";
  262. }
  263. if (! -e "$opt{s}/$repo" ) {
  264. mkdir "$opt{s}/$repo" or die "Failed to create \"$opt{p}/$repo\": $!\n";
  265. }
  266. if (! -e "$opt{s}/$repo/$version" ) {
  267. mkdir "$opt{s}/$repo/$version" or die "Failed to create \"$opt{p}/$repo/$version\": $!\n";
  268. }
  269. else {
  270. die "Refused to overwrite previously unpacked \"$opt{s}/$repo/$version\".\n";
  271. }
  272. # TODO: Verify sig
  273. my $pwd = getcwd();
  274. my $unpackdir = "$opt{s}/$repo/$version";
  275. chdir "$unpackdir";
  276. if ($@) {
  277. my $err = $!;
  278. chdir $pwd;
  279. die "Failed to chdir to \"$unpackdir\": $err\n";
  280. }
  281. undef $!;
  282. system(@$UNZIP, $fn);
  283. if ($? != 0) {
  284. my $err = $!;
  285. chdir $pwd;
  286. die "Failed to unpack \"$unpackdir\"".($err?": $err":".")."\n";
  287. }
  288. chdir $pwd;
  289. # Add where we unpacked it
  290. $_->[3] = $unpackdir;
  291. return 0;
  292. }
  293. sub ruleset_fetch_latest {
  294. my($repo, @type) = @_;
  295. my @versions = ruleset_available_versions($repo);
  296. my $verre = defined($opt{v}) ? qr/^$opt{v}/ : qr/^/;
  297. my $typere = undef;
  298. # Figure out what to look for
  299. if (@type == 1 and $type[0] ne "") {
  300. my $type = $type[0];
  301. if ($type eq "UNSTABLE") {
  302. $typere = qr/\d-\D+\d+$/;
  303. }
  304. else {
  305. $typere = qr/\d-$type\d+$/;
  306. }
  307. }
  308. elsif (@type > 1) {
  309. my $type;
  310. for (@type) {
  311. if ($_ eq "") {
  312. $type .= ($type?"|":"").qr/\.\d+$/;
  313. }
  314. elsif ($_ eq "UNSTABLE") {
  315. $type .= ($type?"|":"").qr/\d-\D+\d+$/;
  316. }
  317. else {
  318. $type .= ($type?"|":"").qr/\d-$_\d+$/;
  319. }
  320. }
  321. $typere = qr/$type/;
  322. }
  323. else {
  324. $typere = qr/\.\d+$/;
  325. }
  326. if ($opt{d}) {
  327. print STDERR "REPO: $repo\n";
  328. print STDERR "TYPES: ".join(", ", @type)."\n";
  329. print STDERR "VERSIONS: ".join(", ", @versions)."\n";
  330. print STDERR "REGEX: version=$opt{v} type=$typere\n";
  331. }
  332. while (@versions) {
  333. my $last = pop(@versions);
  334. # Check REs on version
  335. if ($last =~ m/$opt{v}/ and (!defined($typere) || $last =~ m/$typere/)) {
  336. return ruleset_fetch($repo, $last);
  337. }
  338. if ($opt{d}) {
  339. print STDERR "Skipping version: $last\n";
  340. }
  341. }
  342. die "No '".join("' or '", @type)."' ruleset found.\n";
  343. }
  344. sub notify_email {
  345. my $version_text = join("\n", map { "$_->[0] v$_->[1]".(defined($_->[3])?": $_->[3]":"") } @_);
  346. my $from = $opt{f} ? "From: $opt{f}\n" : "";
  347. my $body = << "EOT";
  348. ModSecurity rulesets updated and ready to install on host $HOST:
  349. $version_text
  350. ModSecurity - http://www.modsecurity.org/
  351. EOT
  352. # TODO: Diffs
  353. open(SM, "|-", @$SENDMAIL) or die "Failed to send mail: $!\n";
  354. print STDERR "Sending notification email to: $opt{e}\n";
  355. print SM << "EOT";
  356. ${from}To: $opt{e}
  357. Subject: [$HOST] ModSecurity Ruleset Update Notification
  358. $body
  359. EOT
  360. close SM;
  361. }
  362. sub ruleset_verifysig {
  363. my($fn, $sigfn) = @_;
  364. print STDERR "Verifying \"$fn\" with signature \"$sigfn\"\n";
  365. my $gpg = new GnuPG();
  366. my $sig = eval { $gpg->verify( signature => $sigfn, file => $fn ) };
  367. if (defined $sig) {
  368. print STDERR sig2str($sig)."\n";
  369. }
  370. if (!defined($sig)) {
  371. die "Signature validation failed.\n";
  372. }
  373. if ( $sig->{trust} < $REQUIRED_SIG_TRUST ) {
  374. die "Signature is not trusted ".$GPG_TRUST{$REQUIRED_SIG_TRUST}.".\n";
  375. }
  376. return;
  377. }
  378. sub sig2str {
  379. my %sig = %{ $_[0] || {} };
  380. "Signature made ".localtime($sig{timestamp})." by $sig{user} (ID: $sig{keyid}) and is $GPG_TRUST{$sig{trust}} trusted.";
  381. }
  382. ################################################################################
  383. ################################################################################
  384. # List what is there
  385. if ($opt{l}) { repository_dump(); exit 0 }
  386. # Latest stable
  387. elsif (defined($opt{S})) { ruleset_fetch_latest($opt{S}, "") }
  388. # Latest development
  389. elsif (defined($opt{D})) { ruleset_fetch_latest($opt{D}, "dev") }
  390. # Latest release candidate
  391. elsif (defined($opt{R})) { ruleset_fetch_latest($opt{R}, "rc") }
  392. # Latest unstable
  393. elsif (defined($opt{U})) { ruleset_fetch_latest($opt{U}, "UNSTABLE") }
  394. # Latest release candidate or stable
  395. elsif (defined($opt{L})) { ruleset_fetch_latest($opt{R}, "rc", "") }
  396. # Latest (any type)
  397. elsif (defined($opt{F})) { ruleset_fetch_latest($opt{F}, undef) }
  398. # Unpack
  399. if ($opt{u}) {
  400. if (! defined $opt{s} ) { usage(1, "LocalRules is required for unpacking.") }
  401. for (@fetched) {
  402. ruleset_unpack($_);
  403. }
  404. }
  405. # Unpack
  406. if ($opt{e}) {
  407. notify_email(@fetched);
  408. }