| 1 | #!/usr/bin/env python |
| 2 | #*************************************************************************** |
| 3 | #* Copyright (C) 2003-2011 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 enc=utf-8: |