tails-security-check 3.68 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
use Locale::TextDomain 'tails';
39 40
use Tails::Download::HTTPS;
use Try::Tiny;
41 42 43 44 45
use XML::Atom;
use XML::Atom::Feed;

### configuration

46
my $default_base_url = 'https://tails.boum.org/security/';
47

Tails developers's avatar
Tails developers committed
48
=head1 FUNCTIONS
49

50 51 52 53 54 55 56 57 58 59 60
=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;
}

61 62
=head2 atom_str

63 64 65 66
Argument: an Atom feed URL

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

67 68 69
=cut
sub atom_str {
    my $url = shift;
70
    assert_defined($url);
71

72 73 74 75 76 77
    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;
78 79
}

80
=head2 get_entries
amnesia's avatar
amnesia committed
81

82
Arguments: the Atom feed URL.
amnesia's avatar
amnesia committed
83

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

86
We use this manual Accept-Language algorithm as the website
87 88
layout does not allow us to use content negotiation.

amnesia's avatar
amnesia committed
89
=cut
90
sub get_entries {
91
    my $base_url = shift;
Tails developers's avatar
Tails developers committed
92 93
    assert_defined($base_url);
    assert_nonblank($base_url);
amnesia's avatar
amnesia committed
94

95 96 97
    my $separator = '';
    $separator = '/' unless $base_url =~ m{/\z}xms;

98
    my @try_urls = (
99 100
        $base_url . $separator . 'index.' . current_lang() . '.atom',
        $base_url . $separator . 'index.en.atom',
101 102 103 104 105 106
    );

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

109
    return XML::Atom::Feed->new(\$feed_str)->entries();
amnesia's avatar
amnesia committed
110 111
}

112 113
=head2 notify_user

114
Notify the user about the Atom entries passed as arguments.
115 116 117 118 119

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

120
    my $body = __('This version of Tails has known security issues:') . "\n";
121

122
    for (@entries) {
123
        $body .= '' . '<a href="' . $_->id . '">' . $_->title . '</a>' . "\n";
124 125 126
    }

    say $body;
127

128 129
    exec(
        qw{/usr/bin/zenity --warning},
130
        q{--ellipsize},
131
        q{--title}, __('Known security issues'),
132 133
        q{--text},  $body,
    );
134 135
}

136 137 138 139 140 141 142
=head2 categories

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

=cut
sub categories {
    my $entry = shift;
143 144 145 146 147 148
    my $ns = XML::Atom::Namespace->new(
        dc => 'http://purl.org/dc/elements/1.1/'
    );
    my @category = ($entry->can('categories'))
        ? $entry->categories
        : $entry->category;
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
    @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');

164
    ! grep { $_ eq 'security/fixed' } categories($entry);
165 166 167 168 169 170 171 172 173 174 175 176 177 178
}

=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;
}

179

180
=head1 MAIN
181

182
=head2 parse command line args
183

184
=cut
185
my $base_url  = shift || $default_base_url;
186
my $opt_since = shift;
Tails developers's avatar
Tails developers committed
187

188

189
=head2 do the work
190

191
=cut
192
my @unfixed_entries = unfixed_entries(get_entries($base_url));
193

194
if (! @unfixed_entries) {
195 196 197
    exit 0;
}
else {
198
    notify_user(@unfixed_entries);
199
}