]> ruderich.org/simon Gitweb - blhc/blhc.git/blob - bin/blhc
Store regex for compiler commands in a global variable.
[blhc/blhc.git] / bin / blhc
1 #!/usr/bin/perl
2
3 # Build log hardening check, checks build logs for missing hardening flags.
4
5 # Copyright (C) 2012  Simon Ruderich
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20
21 use strict;
22 use warnings;
23
24 use Getopt::Long ();
25 use Term::ANSIColor ();
26
27 our $VERSION = '0.01';
28
29
30 # FUNCTIONS
31
32 sub error_flags {
33     my ($message, $missing_flags_ref, $flag_renames_ref, $line) = @_;
34
35     # Rename flags if requested.
36     my @missing_flags = map {
37         (exists $flag_renames_ref->{$_})
38             ? $flag_renames_ref->{$_}
39             : $_
40     } @{$missing_flags_ref};
41
42     my $flags = join ' ', @missing_flags;
43     printf "%s (%s)%s %s",
44            error_color($message, 'red'), $flags, error_color(':', 'yellow'),
45            $line;
46 }
47 sub error_nonverbose_build {
48     my ($line) = @_;
49
50     printf "%s%s %s",
51            error_color('NONVERBOSE BUILD', 'red'),
52            error_color(':', 'yellow'),
53            $line;
54 }
55 sub error_color {
56     my ($message, $color) = @_;
57
58     # Use colors when writing to a terminal.
59     if (-t STDOUT) {
60         return Term::ANSIColor::colored($message, $color);
61     } else {
62         return $message;
63     }
64 }
65
66 sub any_flags_used {
67     my ($line, @flags) = @_;
68
69     foreach my $flag (@flags) {
70         return 1 if $line =~ /\s$flag(\s|\\|$)/;
71     }
72
73     return 0;
74 }
75 sub all_flags_used {
76     my ($line, $missing_flags_ref, @flags) = @_;
77
78     my @missing_flags = ();
79     foreach my $flag (@flags) {
80         if ($line !~ /\s$flag(\s|\\|$)/) {
81             push @missing_flags, $flag;
82         }
83     }
84
85     if (scalar @missing_flags == 0) {
86         return 1;
87     }
88
89     @{$missing_flags_ref} = @missing_flags;
90     return 0;
91 }
92
93 # Modifies $missing_flags_ref array.
94 sub pic_pie_conflict {
95     my ($line, $pie, $missing_flags_ref, @flags_pie) = @_;
96
97     return 0 if not $pie;
98     return 0 if not any_flags_used($line, ('-fPIC', '-fpic'));
99
100     my %flags = map { $_ => 1 } @flags_pie;
101
102     # Remove all PIE flags from @missing_flags as they are not required with
103     # -fPIC.
104     my @result = grep {
105         not exists $flags{$_}
106     } @{$missing_flags_ref};
107     @{$missing_flags_ref} = @result;
108
109     # We got a conflict when no flags are left, thus only PIE flags were
110     # missing. If other flags were missing abort because the conflict is not
111     # the problem.
112     return scalar @result == 0;
113 }
114
115
116 # CONSTANTS/VARIABLES
117
118 # Regex to catch compiler commands.
119 my $cc_regex = qr/((?<!\.)cc|(x86_64-linux-gnu-)?gcc|g\+\+|c\+\+)/;
120
121 # Regex to catch (GCC) compiler warnings.
122 my $warning_regex = qr/^(.+?):([0-9]+):[0-9]+: warning: (.+?) \[(.+?)\]$/;
123
124 # Expected hardening flags. All flags are used as regexps.
125 my @cflags = (
126     '-g',
127     '-O2',
128     '-fstack-protector',
129     '--param=ssp-buffer-size=4',
130     '-Wformat',
131     '-Wformat-security',
132     '-Werror=format-security',
133 );
134 my @cflags_pie = (
135     '-fPIE',
136 );
137 my @cppflags = (
138     '-D_FORTIFY_SOURCE=2',
139 );
140 my @ldflags = (
141     '-Wl,(-z,)?relro',
142 );
143 my @ldflags_pie = (
144     '-fPIE',
145     '-pie',
146 );
147 my @ldflags_bindnow = (
148     '-Wl,(-z,)?now',
149 );
150 # All (hardening) flags.
151 my @flags = (@cflags, @cflags_pie,
152              @cppflags,
153              @ldflags, @ldflags_pie, @ldflags_bindnow);
154 # Renaming rules for the output so the regex parts are not visible.
155 my %flag_renames = (
156     '-Wl,(-z,)?relro' => '-Wl,-z,relro',
157     '-Wl,(-z,)?now'   => '-Wl,-z,now',
158 );
159
160
161 # MAIN
162
163 # Additional hardening options.
164 my $pie     = 0;
165 my $bindnow = 0;
166
167 # Parse command line arguments.
168 my $option_all     = 0;
169 my $option_help    = 0;
170 my $option_version = 0;
171 if (not Getopt::Long::GetOptions(
172             'help|h|?' => \$option_help,
173             'version'  => \$option_version,
174             'pie'      => \$pie,
175             'bindnow'  => \$bindnow,
176             'all'      => \$option_all,
177         )) {
178     require Pod::Usage;
179     Pod::Usage::pod2usage(2);
180 }
181 if ($option_help) {
182     require Pod::Usage;
183     Pod::Usage::pod2usage(1);
184 }
185 if ($option_version) {
186     print "blhc $VERSION  Copyright (C) 2012  Simon Ruderich
187
188 This program is free software: you can redistribute it and/or modify
189 it under the terms of the GNU General Public License as published by
190 the Free Software Foundation, either version 3 of the License, or
191 (at your option) any later version.
192
193 This program is distributed in the hope that it will be useful,
194 but WITHOUT ANY WARRANTY; without even the implied warranty of
195 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
196 GNU General Public License for more details.
197
198 You should have received a copy of the GNU General Public License
199 along with this program.  If not, see <http://www.gnu.org/licenses/>.
200 ";
201     exit 0;
202 }
203
204 if ($option_all) {
205     $pie     = 1;
206     $bindnow = 1;
207 }
208
209 # Final exit code.
210 my $exit = 0;
211
212 # Input lines, contain only the lines with compiler commands.
213 my @input = ();
214
215 my $continuation = 0;
216 while (my $line = <>) {
217     # Ignore compiler warnings for now.
218     next if $line =~ /$warning_regex/;
219
220     # Try to detect non verbose build logs.
221     if ($line =~ /^checking if you want to see long compiling messages\.\.\. no/
222             or $line =~ /^\s*(CC|CCLD)\s+/
223             or $line =~ /^\s*(C|c)ompiling\s+/
224             or $line =~ /^\s*\[[\d ]+%\] Building /) {
225         error_nonverbose_build($line);
226         $exit |= 1 << 2;
227     }
228
229
230     # One line may contain multiple commands (";"). Treat each one as single
231     # line.
232     my @line = split /(?<!\\);/, $line;
233     foreach $line (@line) {
234         # Add newline, drop all other whitespace at the end of a line.
235         $line =~ s/\s+$//;
236         $line .= "\n";
237
238         if ($continuation) {
239             $continuation = 0;
240
241             # Join lines, but leave the "\" in place so it's clear where the
242             # original line break was.
243             chomp $input[-1];
244             $input[-1] .= ' ' . $line;
245
246         } else {
247             # Ignore lines with no compiler commands.
248             next if $line !~ /\b$cc_regex(\s|\\)/;
249
250             # Ignore false positives.
251             #
252             # `./configure` output.
253             if ($line =~ /^checking /) {
254                 next;
255             }
256
257             push @input, $line;
258         }
259
260         # Line continuation, line ends with "\".
261         if ($line =~ /\\\s*$/) {
262             $continuation = 1;
263         }
264     }
265 }
266
267 if (scalar @input == 0) {
268     print "No compiler commands!\n";
269     $exit |= 1;
270     exit $exit;
271 }
272
273 # Check if additional hardening options were used. Used to ensure they are
274 # used for the complete build.
275 foreach my $line (@input) {
276     $pie     = 1 if any_flags_used($line, @cflags_pie, @ldflags_pie);
277     $bindnow = 1 if any_flags_used($line, @ldflags_bindnow);
278 }
279
280 if ($pie) {
281     @cflags  = (@cflags,  @cflags_pie);
282     @ldflags = (@ldflags, @ldflags_pie);
283 }
284 if ($bindnow) {
285     @ldflags = (@ldflags, @ldflags_bindnow);
286 }
287
288 foreach my $line (@input) {
289     # Ignore false positives.
290     #
291     # ./configure summary.
292     next if $line =~ /^\s*(C|c)ompiler[\s.]*:\s+$cc_regex(\s-std=[a-z0-9:+]+)?\s*$/
293             or $line =~ /^\s*- (CC|CXX)\s*=\s*$cc_regex\s*$/
294             or $line =~ /^\s*-- Check for working (C|CXX) compiler: /;
295
296     # Is this a compiler or linker command?
297     my $compiler = 1;
298     my $linker   = 0;
299
300     # Linker commands.
301     if ($line =~ m{\s-o                      # -o
302                    [\s\\]*\s+                # possible line continuation
303                    ([A-Za-z0-9_/.-]+/)?      # path to file
304                    [A-Za-z0-9_-]+            # binary name (no dots!)
305                    ([0-9.]*\.so[0-9.]*[a-z]? # library (including version)
306                     |\.la)?
307                    (\s|\\|\$)                # end of file name
308                   }x
309             or $line =~ /^libtool: link: /
310             or $line =~ m{\s*/bin/bash .+?libtool\s+(.+?\s+)?--mode=(re)?link}) {
311         $compiler = 0;
312         $linker   = 1;
313     }
314
315     # If there are source files then it's compiling/linking in one step and we
316     # must check both.
317     if ($line =~ /\.(c|cc|cpp)\b/) {
318         $compiler = 1;
319     }
320
321     # Check hardening flags.
322     my @missing;
323     if ($compiler and not all_flags_used($line, \@missing, @cflags)
324             # Libraries linked with -fPIC don't have to (and can't) be linked
325             # with -fPIE as well. It's no error if only PIE flags are missing.
326             and not pic_pie_conflict($line, $pie, \@missing, @cflags_pie)) {
327         error_flags('CFLAGS missing', \@missing, \%flag_renames, $line);
328         $exit |= 1 << 3;
329     }
330     if ($compiler and not all_flags_used($line, \@missing, @cppflags)) {
331         error_flags('CPPFLAGS missing', \@missing, \%flag_renames, $line);
332         $exit |= 1 << 3;
333     }
334     if ($linker and not all_flags_used($line, \@missing, @ldflags)
335             # Same here, -fPIC conflicts with -fPIE.
336             and not pic_pie_conflict($line, $pie, \@missing, @ldflags_pie)) {
337         error_flags('LDFLAGS missing', \@missing, \%flag_renames, $line);
338         $exit |= 1 << 3;
339     }
340 }
341
342 exit $exit;
343
344
345 __END__
346
347 =head1 NAME
348
349 blhc - build log hardening check, checks build logs for missing hardening flags
350
351 =head1 SYNOPSIS
352
353 B<blhc> [-h -? --help]
354
355 B<blhc> [--pie] [--bindnow] [--all]
356
357     --help                  available options
358     --version               version number and license
359     --pie                   force +pie check
360     --bindnow               force +bindbow check
361     --all                   force +all (+pie, +bindnow) check
362
363 =head1 DESCRIPTION
364
365 blhc is a small tool which checks build logs for missing hardening flags and
366 other important warnings. It's licensed under the GPL 3 or later.
367
368 =head1 OPTIONS
369
370 =over 8
371
372 =item B<-h -? --help>
373
374 Print available options.
375
376 =item B<--version>
377
378 Print version number and license.
379
380 =item B<--pie>
381
382 Force check for all +pie hardening flags. By default it's auto detected.
383
384 =item B<--bindnow>
385
386 Force check for all +bindnow hardening flags. By default it's auto detected.
387
388 =item B<--all>
389
390 Force check for all +all (+pie, +bindnow) hardening flags. By default it's
391 auto detected.
392
393 =back
394
395 Auto detection only works if at least one command uses the required hardening
396 flag (e.g. -fPIE). Then it's required for all other commands as well.
397
398 =head1 EXIT STATUS
399
400 The exit status is a "bit mask", each listed status is ORed when the error
401 condition occurs to get the result.
402
403 =over 8
404
405 =item B<0>
406
407 Success.
408
409 =item B<1>
410
411 No compiler commands were found.
412
413 =item B<2>
414
415 Invalid arguments/options given to blhc.
416
417 =item B<4>
418
419 Non verbose build.
420
421 =item B<8>
422
423 Missing hardening flags.
424
425 =back
426
427 =head1 AUTHOR
428
429 Simon Ruderich, E<lt>simon@ruderich.orgE<gt>
430
431 =head1 COPYRIGHT AND LICENSE
432
433 Copyright (C) 2012 by Simon Ruderich
434
435 This program is free software: you can redistribute it and/or modify
436 it under the terms of the GNU General Public License as published by
437 the Free Software Foundation, either version 3 of the License, or
438 (at your option) any later version.
439
440 This program is distributed in the hope that it will be useful,
441 but WITHOUT ANY WARRANTY; without even the implied warranty of
442 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
443 GNU General Public License for more details.
444
445 You should have received a copy of the GNU General Public License
446 along with this program.  If not, see <http://www.gnu.org/licenses/>.
447
448 =cut