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