Moving to GitHub.
[platal.git] / bin / check_security_fixes.py
CommitLineData
ced072ea
VZ
1#!/usr/bin/env python
2#***************************************************************************
c441aabe 3#* Copyright (C) 2003-2014 Polytechnique.org *
ced072ea
VZ
4#* http://opensource.polytechnique.org/ *
5#* *
6#* This program is free software; you can redistribute it and/or modify *
7#* it under the terms of the GNU General Public License as published by *
8#* the Free Software Foundation; either version 2 of the License, or *
9#* (at your option) any later version. *
10#* *
11#* This program is distributed in the hope that it will be useful, *
12#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14#* GNU General Public License for more details. *
15#* *
16#* You should have received a copy of the GNU General Public License *
17#* along with this program; if not, write to the Free Software *
18#* Foundation, Inc., *
19#* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20#***************************************************************************/
21
22"""Checks that a working copy of plat/al has all the latest security patches
23applied. It uses the local SECURITY file to determine the list of mandatory
24patches.
25
fc841006
VZ
26Important notice: do not execute this script directly from an automatic checkout
27of plat/al. It would be extremely unwise to execute it with root privileges from
50e2ba89 28a place where everybody can change it!
fc841006 29
ced072ea 30Usage (-w updates the local .htaccess to disable guilty working copies):
50e2ba89 31 check_security_fixes.py [-w] -b REFERENCE_PLATAL PLATAL_TO_CHECK...
ced072ea
VZ
32"""
33
34import optparse
35import os
36import re
37import sys
38import time
39
40
41class WorkingCopy(object):
42 """Helper class for analyzing the state of a working copy, and eventually
43 disabling it if an issue is found.
44
45 It disables the local checkout by updating its .htaccess file to deny all
46 requests with an explicit message which states how to fix the issue.
47 """
48
49 CORE_SECURITY_FILE = 'core/SECURITY'
50 MASTER_SECURITY_FILE = 'SECURITY'
51 SECURITY_FIX_RE = re.compile('^-[0-9]{4}')
52
53 HTACCESS_FILE = 'htdocs/.htaccess'
54 HTACCESS_TEMPLATE = 'Deny from all\nErrorDocument 403 "%s"\n'
55 HTACCESS_MTIME_DELTA = 86400 * 365 * 10
56 ERROR_MESSAGE_LINE = '<li>%s</li>\n'
57 ERROR_MESSAGE_TEMPLATE = """
58 Your local checkout of plat/al has been disabled for security reasons. It
59 appears that several critical flaws known in the plat/al codebase have not
60 been patched in your working copy. These flaws are listed below:
61 <ul>%s</ul>
62
63 Please have a look at the SECURITY and core/SECURITY files in any recent
64 plat/al checkout to get more details on which commits did fix those flaws.
65 <br/><br/>
66
67 <em>Note:</em> you can re-enable your working copy by typing
68 <code>make</code> in the root directory of your checkout (usually in
69 <code>~/dev/platal</code>).
70 """
71
72 def __init__(self, reference_path, checkout_path):
73 self.reference_path = reference_path
74 self.checkout_path = checkout_path
75
76 def GetPartialSecurityDiff(self, security_file):
77 """Diffs the reference and a local SECURITY file to find missing security
78 fixes. It filters out the diff result to extract the list of fixes."""
79
80 ref_file = os.path.join(self.reference_path, security_file)
81 wc_file = os.path.join(self.checkout_path, security_file)
82
a959b199 83 diff = os.popen('diff -NBw -U 0 %s %s' % (ref_file, wc_file))
ced072ea
VZ
84 for line in diff.readlines():
85 if self.SECURITY_FIX_RE.match(line):
86 yield line[1:-1]
87
88 def GetSecurityDiff(self):
89 """Retrieves the missing security patches for various parts of plat/al."""
90
91 missing_fixes = []
92 missing_fixes.extend(self.GetPartialSecurityDiff(self.CORE_SECURITY_FILE))
93 missing_fixes.extend(self.GetPartialSecurityDiff(self.MASTER_SECURITY_FILE))
94 return missing_fixes
95
96 def GetErrorMessage(self, missing_fixes):
97 """Returns a the .htaccess HTML error message.
98
99 It builds an HTML message explaining why the working copy was disabled, how
100 to fix the underlying issues, and how to re-enable it."""
101
102 fixes_list = map(lambda item: self.ERROR_MESSAGE_LINE % item, missing_fixes)
103 return self.ERROR_MESSAGE_TEMPLATE % '\n'.join(fixes_list)
104
105 def Write403Htaccess(self, html_content):
106 """Updates the .htaccess to disable all requests, using |html_content| as
107 the error message. It also sets a modification time in the past to ensure
108 that any subsquent call to 'make' on the wc will actually overwrite the
109 .htaccess file."""
110
111 htaccess = os.path.join(self.checkout_path, self.HTACCESS_FILE)
112 ht_fd = open(htaccess, 'w')
113 ht_fd.write(self.HTACCESS_TEMPLATE % (html_content
114 .replace('\\', '\\\\')
115 .replace('"', '\\"')
116 .replace('\n', '\\\n')))
117 ht_fd.close()
118
119 mtime = time.time() - self.HTACCESS_MTIME_DELTA
120 os.utime(htaccess, (mtime, mtime))
121
122 def CheckAndDisableWorkingCopy(self, disable_when_flawed):
123 """Checks that the local working copy is in a sane state. If not, warns the
124 user by printing a message to the console, and disables the wc if
125 |disable_when_flawed| is set to true."""
126
127 missing_fixes = self.GetSecurityDiff()
128 if len(missing_fixes):
129 # Warn the user on the standard output.
130 print "Found %d missing security fixes in %s:" % (len(missing_fixes),
131 self.checkout_path)
132 for issue in missing_fixes:
133 print " * %s" % issue
134
135 # Disable the working copy.
136 if disable_when_flawed:
137 print "Disabling working copy in %s." % self.checkout_path
138 self.Write403Htaccess(self.GetErrorMessage(missing_fixes))
139
fc841006
VZ
140def SelfCheckIsLatestVersion(base_path):
141 """Checks that this script is the latest available by comparing itself to
142 the reference script in |base_path|. It is important to do that check as
143 most deployment will want to execute this script with root privileges,
144 which implies that this script is deployed in a safe directory, and not
145 just executed from an automatically updated checkout of plat/al (how
50e2ba89 146 unsafe would that be...)."""
fc841006
VZ
147
148 base_script = os.path.join(base_path, 'bin/check_security_fixes.py')
149 local_script = os.path.abspath(sys.argv[0])
150
151 if os.system('diff -q %s %s' % (base_script, local_script)) != 0:
152 sys.stderr.write('Please upgrade this script to the latest version.\n')
ced072ea
VZ
153
154def main():
155 parser = optparse.OptionParser()
156 parser.add_option('-b', '--base_path', action='store', dest='base_path')
157 parser.add_option('-w', '--write_htaccess', action='store_true',
158 dest='write_htaccess', default=False)
159 (options, args) = parser.parse_args()
160
161 if options.base_path is None:
162 print "Error: option --base_path (or -b) is required for the script to run."
163 sys.exit(1)
164 if not os.path.exists(os.path.join(options.base_path,
165 WorkingCopy.MASTER_SECURITY_FILE)):
166 print "The base plat/al (%s) is too old to be used." % options.base_path
167 sys.exit(1)
168
fc841006 169 SelfCheckIsLatestVersion(options.base_path)
ced072ea
VZ
170 for platal in args:
171 wc = WorkingCopy(options.base_path, platal)
172 wc.CheckAndDisableWorkingCopy(options.write_htaccess)
173
174if __name__ == '__main__':
175 main()
fc841006 176
448c8cdc 177# vim:set et sw=2 sts=2 sws=2 fenc=utf-8: