Moving to GitHub.
[platal.git] / bin / check_security_fixes.py
1 #!/usr/bin/env python
2 #***************************************************************************
3 #* Copyright (C) 2003-2014 Polytechnique.org *
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
23 applied. It uses the local SECURITY file to determine the list of mandatory
24 patches.
25
26 Important notice: do not execute this script directly from an automatic checkout
27 of plat/al. It would be extremely unwise to execute it with root privileges from
28 a place where everybody can change it!
29
30 Usage (-w updates the local .htaccess to disable guilty working copies):
31 check_security_fixes.py [-w] -b REFERENCE_PLATAL PLATAL_TO_CHECK...
32 """
33
34 import optparse
35 import os
36 import re
37 import sys
38 import time
39
40
41 class 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
83 diff = os.popen('diff -NBw -U 0 %s %s' % (ref_file, wc_file))
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
140 def 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
146 unsafe would that be...)."""
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')
153
154 def 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
169 SelfCheckIsLatestVersion(options.base_path)
170 for platal in args:
171 wc = WorkingCopy(options.base_path, platal)
172 wc.CheckAndDisableWorkingCopy(options.write_htaccess)
173
174 if __name__ == '__main__':
175 main()
176
177 # vim:set et sw=2 sts=2 sws=2 fenc=utf-8: