tails-security-check 3.74 KB
Newer Older
1 2 3
#! /usr/bin/perl

use strict;
4
use warnings FATAL => 'all';
5
use 5.10.1;
6 7 8 9 10

#man{{{

=head1 NAME

11
tails-security-check
12 13 14 15 16 17 18 19

=cut


=head1 DESCRIPTION

=head1 SYNOPSIS

20
tails-security-check [ ATOM_FEED_BASE_URL ]
21 22 23 24

  ATOM_FEED_BASE_URL will be appended /index.XX.atom,
  for XX in (current locale's language code, 'en'),
  until success is reported by the HTTP layer.
25 26 27

=head1 AUTHOR

Tails developers's avatar
Tails developers committed
28
Tails developers <tails@boum.org>
29
See https://tails.boum.org/.
30 31 32 33 34 35

=cut

#}}}

use Carp;
36
use Carp::Assert::More;
37
use Fatal qw{open close};
38 39
use Locale::gettext;
use POSIX;
40 41
use Tails::Download::HTTPS;
use Try::Tiny;
42 43 44
use XML::Atom;
use XML::Atom::Feed;

45
setlocale(LC_MESSAGES, "");
46
textdomain("tails");
47

48 49
### configuration

50
my $default_base_url = 'https://tails.boum.org/security/';
51

Tails developers's avatar
Tails developers committed
52
=head1 FUNCTIONS
53

54 55 56 57 58 59 60 61 62 63 64
=head2 current_lang

Returns the two-letters language code of the current session.

=cut
sub current_lang {
    my ($code) = ($ENV{LANG} =~ m/([a-z]{2}).*/);

    return $code;
}

65 66
=head2 atom_str

67 68 69 70
Argument: an Atom feed URL

Returns the Atom's feed content on success, undef on failure.

71 72 73
=cut
sub atom_str {
    my $url = shift;
74
    assert_defined($url);
75

76 77 78 79 80 81
    my $downloader = Tails::Download::HTTPS->new(
        max_download_size => 256 * 2**10,
    );
    my $content;
    try { $content = $downloader->get_url($url); };
    defined $content ? return $content : return undef;
82 83
}

84
=head2 get_entries
amnesia's avatar
amnesia committed
85

86
Arguments: the Atom feed URL.
amnesia's avatar
amnesia committed
87

88
Returns the list of XML::Atom::Entry objects from the feed.
amnesia's avatar
amnesia committed
89

90
We use this manual Accept-Language algorithm as the website
91 92
layout does not allow us to use content negotiation.

amnesia's avatar
amnesia committed
93
=cut
94
sub get_entries {
95
    my $base_url = shift;
Tails developers's avatar
Tails developers committed
96 97
    assert_defined($base_url);
    assert_nonblank($base_url);
amnesia's avatar
amnesia committed
98

99 100 101
    my $separator = '';
    $separator = '/' unless $base_url =~ m{/\z}xms;

102
    my @try_urls = (
103 104
        $base_url . $separator . 'index.' . current_lang() . '.atom',
        $base_url . $separator . 'index.en.atom',
105 106 107 108 109 110
    );

    my $feed_str;
    foreach my $url (@try_urls) {
        last if ($feed_str = atom_str($url));
    }
111
    assert_defined($feed_str);
112

113
    return XML::Atom::Feed->new(\$feed_str)->entries();
amnesia's avatar
amnesia committed
114 115
}

116 117
=head2 notify_user

118
Notify the user about the Atom entries passed as arguments.
119 120 121 122 123

=cut
sub notify_user {
    my @entries = @_;

124
    my $body = gettext('This version of Tails has known security issues:') . "\n";
125

126
    for (@entries) {
127
        $body .= '' . '<a href="' . $_->id . '">' . $_->title . '</a>' . "\n";
128 129 130
    }

    say $body;
131

132 133
    exec(
        qw{/usr/bin/zenity --warning},
134
        q{--ellipsize},
135 136 137
        q{--title}, gettext('Known security issues'),
        q{--text},  $body,
    );
138 139
}

140 141 142 143 144 145 146
=head2 categories

Return the list of categories of the input XML::Atom::Entry object.

=cut
sub categories {
    my $entry = shift;
147 148 149 150 151 152
    my $ns = XML::Atom::Namespace->new(
        dc => 'http://purl.org/dc/elements/1.1/'
    );
    my @category = ($entry->can('categories'))
        ? $entry->categories
        : $entry->category;
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
    @category
        ? (map { $_->label || $_->term } @category)
        : $entry->getlist($ns, 'subject');
}

=head2 is_not_fixed

Returns true iff. the input XML::Atom::Entry object hasn't the
security/fixed tag.

=cut
sub is_not_fixed {
    my $entry = shift;
    assert_isa($entry, 'XML::Atom::Entry');

168
    ! grep { $_ eq 'security/fixed' } categories($entry);
169 170 171 172 173 174 175 176 177 178 179 180 181 182
}

=head2 unfixed_entries

Filter the input list of XML::Atom::Entry objects to only keep entries
that are not marked as fixed yet.

=cut
sub unfixed_entries {
    my @entries = @_;

    grep { is_not_fixed($_) } @entries;
}

183

184
=head1 MAIN
185

186
=head2 parse command line args
187

188
=cut
189
my $base_url  = shift || $default_base_url;
190
my $opt_since = shift;
Tails developers's avatar
Tails developers committed
191

192

193
=head2 do the work
194

195
=cut
196
my @unfixed_entries = unfixed_entries(get_entries($base_url));
197

198
if (! @unfixed_entries) {
199 200 201
    exit 0;
}
else {
202
    notify_user(@unfixed_entries);
203
}