Commit | Line | Data |
---|---|---|
ced072ea VZ |
1 | #!/usr/bin/env python |
2 | #*************************************************************************** | |
5e1513f6 | 3 | #* Copyright (C) 2003-2011 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 | |
23 | applied. It uses the local SECURITY file to determine the list of mandatory | |
24 | patches. | |
25 | ||
fc841006 VZ |
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 | |
50e2ba89 | 28 | a place where everybody can change it! |
fc841006 | 29 | |
ced072ea | 30 | Usage (-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 | ||
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 | ||
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 |
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 | |
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 | |
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 | ||
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 | ||
174 | if __name__ == '__main__': | |
175 | main() | |
fc841006 VZ |
176 | |
177 | # vim:set et sw=2 sts=2 sws=2 enc=utf-8: |