diff --git a/scripts/leaking_addresses.pl b/scripts/leaking_addresses.pl
index 0aac03a020a8f2a37a053377c7e3a8b8174e1914..4610ad3c80c2121ad7dbf6f8f649f995dc9959c7 100755
--- a/scripts/leaking_addresses.pl
+++ b/scripts/leaking_addresses.pl
@@ -31,6 +31,13 @@ my @DIRS = ('/proc', '/sys');
 # Command line options.
 my $help = 0;
 my $debug = 0;
+my $raw = 0;
+my $output_raw = "";	# Write raw results to file.
+my $input_raw = "";	# Read raw results from file instead of scanning.
+
+my $suppress_dmesg = 0;		# Don't show dmesg in output.
+my $squash_by_path = 0;		# Summary report grouped by absolute path.
+my $squash_by_filename = 0;	# Summary report grouped by filename.
 
 # Do not parse these files (absolute path).
 my @skip_parse_files_abs = ('/proc/kmsg',
@@ -73,13 +80,31 @@ sub help
 	my ($exitcode) = @_;
 
 	print << "EOM";
+
 Usage: $P [OPTIONS]
 Version: $V
 
 Options:
 
-	-d, --debug                Display debugging output.
-	-h, --help, --version      Display this help and exit.
+	-o, --output-raw=<file>  Save results for future processing.
+	-i, --input-raw=<file>   Read results from file instead of scanning.
+	    --raw                Show raw results (default).
+	    --suppress-dmesg     Do not show dmesg results.
+	    --squash-by-path     Show one result per unique path.
+	    --squash-by-filename Show one result per unique filename.
+	-d, --debug              Display debugging output.
+	-h, --help, --version    Display this help and exit.
+
+Examples:
+
+	# Scan kernel and dump raw results.
+	$0
+
+	# Scan kernel and save results to file.
+	$0 --output-raw scan.out
+
+	# View summary report.
+	$0 --input-raw scan.out --squash-by-filename
 
 Scans the running (64 bit) kernel for potential leaking addresses.
 
@@ -90,11 +115,33 @@ EOM
 GetOptions(
 	'd|debug'		=> \$debug,
 	'h|help'		=> \$help,
-	'version'		=> \$help
+	'version'		=> \$help,
+	'o|output-raw=s'        => \$output_raw,
+	'i|input-raw=s'         => \$input_raw,
+	'suppress-dmesg'        => \$suppress_dmesg,
+	'squash-by-path'        => \$squash_by_path,
+	'squash-by-filename'    => \$squash_by_filename,
+	'raw'                   => \$raw,
 ) or help(1);
 
 help(0) if ($help);
 
+if ($input_raw) {
+	format_output($input_raw);
+	exit(0);
+}
+
+if (!$input_raw and ($squash_by_path or $squash_by_filename)) {
+	printf "\nSummary reporting only available with --input-raw=<file>\n";
+	printf "(First run scan with --output-raw=<file>.)\n";
+	exit(128);
+}
+
+if ($output_raw) {
+	open my $fh, '>', $output_raw or die "$0: $output_raw: $!\n";
+	select $fh;
+}
+
 parse_dmesg();
 walk(@DIRS);
 
@@ -239,3 +286,141 @@ sub walk
 		}
 	}
 }
+
+sub format_output
+{
+	my ($file) = @_;
+
+	# Default is to show raw results.
+	if ($raw or (!$squash_by_path and !$squash_by_filename)) {
+		dump_raw_output($file);
+		return;
+	}
+
+	my ($total, $dmesg, $paths, $files) = parse_raw_file($file);
+
+	printf "\nTotal number of results from scan (incl dmesg): %d\n", $total;
+
+	if (!$suppress_dmesg) {
+		print_dmesg($dmesg);
+	}
+
+	if ($squash_by_filename) {
+		squash_by($files, 'filename');
+	}
+
+	if ($squash_by_path) {
+		squash_by($paths, 'path');
+	}
+}
+
+sub dump_raw_output
+{
+	my ($file) = @_;
+
+	open (my $fh, '<', $file) or die "$0: $file: $!\n";
+	while (<$fh>) {
+		if ($suppress_dmesg) {
+			if ("dmesg:" eq substr($_, 0, 6)) {
+				next;
+			}
+		}
+		print $_;
+	}
+	close $fh;
+}
+
+sub parse_raw_file
+{
+	my ($file) = @_;
+
+	my $total = 0;          # Total number of lines parsed.
+	my @dmesg;              # dmesg output.
+	my %files;              # Unique filenames containing leaks.
+	my %paths;              # Unique paths containing leaks.
+
+	open (my $fh, '<', $file) or die "$0: $file: $!\n";
+	while (my $line = <$fh>) {
+		$total++;
+
+		if ("dmesg:" eq substr($line, 0, 6)) {
+			push @dmesg, $line;
+			next;
+		}
+
+		cache_path(\%paths, $line);
+		cache_filename(\%files, $line);
+	}
+
+	return $total, \@dmesg, \%paths, \%files;
+}
+
+sub print_dmesg
+{
+	my ($dmesg) = @_;
+
+	print "\ndmesg output:\n";
+
+	if (@$dmesg == 0) {
+		print "<no results>\n";
+		return;
+	}
+
+	foreach(@$dmesg) {
+		my $index = index($_, ': ');
+		$index += 2;    # skid ': '
+		print substr($_, $index);
+	}
+}
+
+sub squash_by
+{
+	my ($ref, $desc) = @_;
+
+	print "\nResults squashed by $desc (excl dmesg). ";
+	print "Displaying [<number of results> <$desc>], <example result>\n";
+
+	if (keys %$ref == 0) {
+		print "<no results>\n";
+		return;
+	}
+
+	foreach(keys %$ref) {
+		my $lines = $ref->{$_};
+		my $length = @$lines;
+		printf "[%d %s] %s", $length, $_, @$lines[0];
+	}
+}
+
+sub cache_path
+{
+	my ($paths, $line) = @_;
+
+	my $index = index($line, ': ');
+	my $path = substr($line, 0, $index);
+
+	$index += 2;            # skip ': '
+	add_to_cache($paths, $path, substr($line, $index));
+}
+
+sub cache_filename
+{
+	my ($files, $line) = @_;
+
+	my $index = index($line, ': ');
+	my $path = substr($line, 0, $index);
+	my $filename = basename($path);
+
+	$index += 2;            # skip ': '
+	add_to_cache($files, $filename, substr($line, $index));
+}
+
+sub add_to_cache
+{
+	my ($cache, $key, $value) = @_;
+
+	if (!$cache->{$key}) {
+		$cache->{$key} = ();
+	}
+	push @{$cache->{$key}}, $value;
+}