├── README.org └── mspatch.py /README.org: -------------------------------------------------------------------------------- 1 | * mapatch.py 2 | 3 | This tiny tool is a wrapper of [[https://code.google.com/p/ms-patch-tools/][iDefense's ms-patch-tools]], which "consists of several tools for extracting useful information from Microsoft bulletins". Basically it adds support of downloading the patch, feature like Darungrim's Bin Collecter. 4 | 5 | ** features(todo) 6 | 7 | *** download specific patch 8 | 9 | *** track replaced bulletins 10 | 11 | *** TODO auto extract 12 | 13 | *** TODO auto diff? 14 | 15 | -------------------------------------------------------------------------------- /mspatch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | mspatch.py 6 | 7 | TODO 8 | """ 9 | __author__ = 'Binjo' 10 | __version__ = '0.1' 11 | __date__ = '2012-03-18 14:19:24' 12 | 13 | import os, sys 14 | import optparse 15 | from msPatchInfo import * 16 | import popen2 17 | 18 | class PatchExtracter(): 19 | """klass of PatchExtracter 20 | """ 21 | 22 | def __init__(self, fpatch=None, tmpdir='extracted'): 23 | """ 24 | """ 25 | self._patchee = fpatch 26 | self._tmpdir = tmpdir 27 | if not os.path.isdir( tmpdir ): 28 | os.makedirs( tmpdir ) 29 | 30 | @property 31 | def tmpdir(self): 32 | """ 33 | """ 34 | return self._tmpdir 35 | 36 | def extract(self, fpatch=None): 37 | """ 38 | """ 39 | patchee = fpatch if fpatch else self._patchee 40 | if not patchee: 41 | raise Exception( 'Patch file missed?' ) 42 | 43 | # TODO handle msu/msp/msi 44 | if patchee[-4:] == '.exe': 45 | popen2.popen2( patchee + " /x:" + self._tmpdir + " /quiet" ) 46 | return True 47 | 48 | return False 49 | 50 | class MsPatchWrapper(msPatchFileInfo): 51 | """wrapper klass of mspatchfileinfo 52 | """ 53 | 54 | def __init__(self, proxy=None): 55 | """ 56 | """ 57 | msPatchFileInfo.__init__(self) 58 | self.BR.set_handle_robots(False) # don't follow rule of robots.txt 59 | if proxy is not None: 60 | self.BR.set_proxies( proxy ) # proxy = {'http':'xxxx:port'} 61 | 62 | self.soup = None 63 | 64 | def getBulletinFileInfo(self, year, num): 65 | 66 | print "-[Retrieving file information for bulletin MS%.2d-%.3d" % (year, num) 67 | 68 | # if year > 11: # FIXME 69 | # url = 'http://technet.microsoft.com/security/bulletin/MS%.2d-%.3d' % (year, num) 70 | # else: 71 | # url = 'http://www.microsoft.com/technet/security/Bulletin/MS%.2d-%.3d.mspx' % (year, num) 72 | url = 'http://technet.microsoft.com/en-us/security/bulletin/ms%.2d-%.3d' % (year, num) 73 | 74 | print "--[Retrieving %s" % (url) 75 | 76 | soup = self.makeSoup(url) 77 | 78 | prevbulletins = ', '.join( self.prevbulletins ) 79 | if prevbulletins: 80 | print '--[Replaced Bulletins (%s)' % (prevbulletins) 81 | 82 | if year > 8 or (year == 8 and num >= 18): 83 | return self.getNewFileInfo(soup, year, num) 84 | else: 85 | return self.getOldFileInfo(soup, year, num) 86 | 87 | def query_bulletin(self, year, num): 88 | 89 | print "-[Retrieving file information for bulletin MS%.2d-%.3d" % (year, num) 90 | 91 | # if year > 11: # FIXME 92 | # url = 'http://technet.microsoft.com/security/bulletin/MS%.2d-%.3d' % (year, num) 93 | # else: 94 | # url = 'http://www.microsoft.com/technet/security/Bulletin/MS%.2d-%.3d.mspx' % (year, num) 95 | url = 'http://technet.microsoft.com/en-us/security/bulletin/ms%.2d-%.3d' % (year, num) 96 | 97 | print "--[Retrieving %s" % (url) 98 | 99 | self.soup = self.makeSoup(url) 100 | 101 | prevbulletins = ', '.join( self.prevbulletins ) 102 | if prevbulletins: 103 | print '--[Replaced Bulletins (%s)' % (prevbulletins) 104 | 105 | @property 106 | def familyids(self): 107 | """ 108 | """ 109 | for x in self.BR.links( url_regex='details\.aspx\?familyid=' ): 110 | txt = x.text 111 | url = x.url 112 | fid = url[url.find('=')+1:] 113 | if fid.find( '&' ) != -1: 114 | fid = fid[:fid.find('&')] 115 | # skip Windows Installer's familyid 116 | if fid in ['5A58B56F-60B6-4412-95B9-54D056D6F9F4', '889482FC-5F56-4A38-B838-DE776FD4138C', ]: 117 | continue 118 | yield txt, fid 119 | 120 | @property 121 | def prevbulletins(self): 122 | """a generator of 'Bulletins Replaced by this Update' 123 | """ 124 | tmp = [ x.text for x in self.BR.links( url_regex='go\.microsoft', text_regex='MS\d+\-' ) ] 125 | tmp = list(set(tmp)) # remove duplicaitons 126 | for x in tmp: 127 | yield x 128 | 129 | def get_patch(self, family, direktory, extract_p=False): 130 | """ 131 | """ 132 | link = 'http://www.microsoft.com/downloads/en/confirmation.aspx?familyid=' + family + '&displayLang=en' 133 | 134 | if not os.path.isdir( direktory ): 135 | os.makedirs( direktory ) 136 | 137 | self.makeSoup( link ) 138 | 139 | downloads = [ x for x in self.BR.links( text_regex='Start download' )] 140 | 141 | for x in downloads: 142 | link = x.url 143 | print '---[Downloading %s' % (link) 144 | try: 145 | rc = self.BR.open( link ) 146 | size = rc.info().getheader( 'content-length' ) 147 | # awkward fix of duplicate patch file name 148 | fn = tmpname = link[link.rfind('/')+1:] 149 | i = 1 150 | while True: 151 | fn = os.path.join( direktory, tmpname ) 152 | if not os.path.isfile( fn ): 153 | break 154 | else: 155 | # same file name exists 156 | if int(size) == os.stat( fn )[6]: 157 | raise Exception( "differ url, same file tho?" ) 158 | else: 159 | tmpname = (tmpname[::-1] + '-' + str(i))[::-1] 160 | i += 1 161 | data = rc.get_data() 162 | except Exception, e: 163 | print '---[...(%s)' % (str(e)) 164 | continue 165 | 166 | if data and len(data) > 0: 167 | with open( fn, 'wb' ) as fh: 168 | fh.write( data ) 169 | print '---[%s saved...' % (fn) 170 | 171 | if extract_p: 172 | ex = PatchExtracter( tmpdir=os.path.join(direktory, "extracted", tmpname[:-4]) ) 173 | try: 174 | if ex.extract( fn ): 175 | print '---[Extracted in (%s)' % (ex.tmpdir) 176 | except Exception, e: 177 | print str(e) 178 | 179 | def main(): 180 | """TODO 181 | """ 182 | opt = optparse.OptionParser( usage="usage: %prog [options]", version="%prot " + __version__ ) 183 | opt.add_option( "-y", "--year", help="year of bulletin" ) 184 | opt.add_option( "-n", "--num", help="number string of bulletin" ) 185 | opt.add_option( "-d", "--download", help="flag as download action", action="store_true", default=False ) 186 | opt.add_option( "-o", "--output", help="output directory", default="patches" ) 187 | opt.add_option( "-l", "--list", help="list familyids", action="store_true", default=False ) 188 | opt.add_option( "-m", "--match", help="string to match target" ) 189 | opt.add_option( "-e", "--extract", help="flag as extract action", action="store_true", default=False ) 190 | opt.add_option( "-f", "--follow", help="follow replaced bulletins", action="store_true", default=False ) 191 | 192 | (opts, args) = opt.parse_args() 193 | 194 | if not opts.year or not opts.num: 195 | opt.print_help() 196 | sys.exit(-1) 197 | 198 | mspatch = MsPatchWrapper() 199 | 200 | if opts.download: 201 | 202 | mspatch.query_bulletin( int(opts.year), int(opts.num) ) 203 | 204 | prevbulletins = [ x for x in mspatch.prevbulletins ] 205 | 206 | for familyid in mspatch.familyids: 207 | if ( opts.match and opts.match.lower() not in familyid[0].lower() ): 208 | continue 209 | 210 | print '---[Target (%s)' % (familyid[0]) 211 | mspatch.get_patch( familyid[1], opts.output, opts.extract ) 212 | 213 | if opts.follow: 214 | 215 | # FIXME follow once? 216 | for prevbulletin in prevbulletins: 217 | m = re.match( 'MS((?P\d+))-((?P\S+))', prevbulletin ) 218 | if not m: 219 | print '----[wtf?? (%s)' % (prevbulletin) 220 | continue 221 | 222 | print '----[Following bulletin (%s)' % (prevbulletin) 223 | 224 | mspatch.query_bulletin( int(m.group('year')), int(m.group('num')) ) 225 | 226 | for familyid in mspatch.familyids: 227 | if ( opts.match and opts.match.lower() not in familyid[0].lower() ): 228 | continue 229 | 230 | print '---[Target (%s)' % (familyid[0]) 231 | mspatch.get_patch( familyid[1], os.path.join(opts.output, prevbulletin), opts.extract ) 232 | 233 | elif opts.list: 234 | 235 | mspatch.query_bulletin( int(opts.year), int(opts.num) ) 236 | 237 | for (target, phamilyid) in mspatch.familyids: 238 | print '|%-40s | %-40s' % (phamilyid, target) 239 | 240 | else: 241 | 242 | results = mspatch.getBulletinFileInfo( int(opts.year), int(opts.num) ) 243 | res = mspatch.generateOutput( results ) 244 | res = ''.join(filter(lambda x:x in string.printable, res)) 245 | print res 246 | 247 | #------------------------------------------------------------------------------- 248 | if __name__ == '__main__': 249 | main() 250 | #------------------------------------------------------------------------------- 251 | # EOF 252 | --------------------------------------------------------------------------------