apt-sec.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. #!/usr/bin/python3
  2. ## Based on the perl code of Trustminer by CASED
  3. ## Nikos
  4. import sys
  5. import os
  6. from pymongo import MongoClient
  7. #mongodb assumes database at default path
  8. import logging, sys
  9. import configparser
  10. import json
  11. import urllib.request
  12. import datetime
  13. import debian_advisory as da
  14. import cveparse as cv
  15. import matplotlib.pyplot as plt
  16. import numpy as np
  17. from dateutil import parser
  18. logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
  19. ## Increase the recursion limit by much to allow bs to parse large files
  20. ## This is not good practise
  21. sys.setrecursionlimit(6000)
  22. #load config file as library
  23. config = configparser.ConfigParser()
  24. config.read('config_test')
  25. if config.sections == []:
  26. print('configuration file not found\n')
  27. sys.exit(1)
  28. #global variables
  29. secperday = 60*60*24
  30. now = datetime.datetime.now()
  31. verbosity = 1
  32. ###############################################################################
  33. ## logging
  34. # 1 fatal errors
  35. # 2 errors
  36. # 3 note
  37. # 4 trace
  38. # 5 debug
  39. def msg(lvl,msg):
  40. if lvl <= int(config['LOG']['loglevel']):
  41. print(msg)
  42. def debug(msg):
  43. msg(5, msg)
  44. # Need to see if this is necessary
  45. ## load state, different from DBs in that we always need it
  46. def load_state():
  47. cache = config['DIR']['cache_dir'] + 'state'
  48. err = 0
  49. state = dict()
  50. try:
  51. with open(cache) as json_data:
  52. state = json.load(json_data)
  53. except FileNotFoundError:
  54. # Load default state - start from the beginning
  55. state['cache_dir'] = cache
  56. state['next_adv'] = 0
  57. state['next_fsa'] = 0
  58. state['Packages'] = ''
  59. state['Sources'] = ''
  60. state['Sha1Sums'] = ''
  61. err += 1
  62. return (state, err)
  63. ###############################################################################
  64. ## save state, different from DBs in that we always need it
  65. def save_state(state):
  66. cache = config['DIR']['cache_dir'] + 'state'
  67. try:
  68. with open(cache, 'w') as fp:
  69. json.dump(state, fp)
  70. except IOError:
  71. print('write cache state failed!! Fatal error')
  72. sys.exit(1)
  73. ###############################################################################
  74. ## load sha lists :TODO later
  75. def load_sha1lists():
  76. cache = config['DIR']['cache_dir'] + 'state'
  77. ###############################################################################
  78. ## save sha lists :TODO later
  79. def save_sha1lists():
  80. pass
  81. ###############################################################################
  82. ## load from files
  83. def load_DBs():
  84. dsatable = dict()
  85. src2dsa = dict()
  86. dsa2cve = dict()
  87. cvetable = dict()
  88. cache = config['DIR']['cache_dir']
  89. cache_dsatable = cache + 'dsatable'
  90. try:
  91. with open(cache_dsatable) as fp:
  92. dsatable = json.load(fp)
  93. except (IOError, ValueError):
  94. print('read cache dsatable failed!! Maybe first run of the system?')
  95. cache_src2dsa = cache + 'src2dsa'
  96. try:
  97. with open(cache_src2dsa) as fp:
  98. src2dsa = json.load(fp)
  99. except (IOError, ValueError):
  100. print('read cache src2dsa failed!! Maybe first run of the system?')
  101. cache_dsa2cve = cache + 'dsa2cve'
  102. try:
  103. with open(cache_dsa2cve) as fp:
  104. dsa2cve = json.load(fp)
  105. except (IOError, ValueError):
  106. print('read cache dsa2cve failed!! Maybe first run of the system?')
  107. cache_cvetable = cache + 'cvetable'
  108. try:
  109. with open(cache_cvetable) as fp:
  110. cvetable = json.load(fp)
  111. except (IOError, ValueError):
  112. print('read cache cvetable failed!! Maybe first run of the system?')
  113. return(dsatable, src2dsa, dsa2cve, cvetable)
  114. ###############################################################################
  115. ## help for save_DBs
  116. def myconverter(o):
  117. if isinstance(o, datetime.datetime) or isinstance(o, datetime.timedelta):
  118. return str(o)
  119. ###############################################################################
  120. ## save to files
  121. def save_DBs(dsatable, src2dsa, dsa2cve, cvetable):
  122. cache = config['DIR']['cache_dir']
  123. cache_dsatable = cache + 'dsatable'
  124. try:
  125. with open(cache_dsatable, 'w') as fp:
  126. json.dump(dsatable, fp, default = myconverter)
  127. except IOError:
  128. print('write cache dsatable failed!! Fatal error')
  129. sys.exit(1)
  130. cache_src2dsa = cache + 'src2dsa'
  131. try:
  132. with open(cache_src2dsa, 'w') as fp:
  133. json.dump(src2dsa, fp)
  134. except IOError:
  135. print('write cache src2dsa failed!! Fatal error')
  136. sys.exit(1)
  137. cache_dsa2cve = cache + 'dsa2cve'
  138. try:
  139. with open(cache_dsa2cve, 'w') as fp:
  140. json.dump(dsa2cve, fp)
  141. except IOError:
  142. print('write cache dsa2cve failed!! Fatal error')
  143. sys.exit(1)
  144. cache_cvetable = cache + 'cvetable'
  145. try:
  146. with open(cache_cvetable, 'w') as fp:
  147. json.dump(cvetable, fp, default = myconverter)
  148. except IOError:
  149. print('write cache cvetable failed!! Fatal error')
  150. sys.exit(1)
  151. ###############################################################################
  152. ## Fetch current Packages, Sources and sha1sums files
  153. ## These are needed to find CVE stats by sha1sums/pkg-names
  154. ## Only Sha1Sums is custom generated, others are from Debian.
  155. ## FIXME: Server might do on-the-fly gzip (but should not for bzip2)
  156. ## Return: 1 on success, to signal that new parsing is needed.
  157. def fetchMeta(filename):
  158. urlbase = config['URL']['pkg_base_url']
  159. mydir = config['DIR']['cache_dir']
  160. bzFile = filename + '.bz2'
  161. url = urlbase + bzFile
  162. logging.info('Checking meta file from ' + url + '\n')
  163. # Download file
  164. urllib.request.urlretrieve(url, mydir + bzfile)
  165. # TODO catch exceptions like file not found
  166. # TODO check if file has changed, if it is new unpack
  167. ###############################################################################
  168. # Sources and Packages are not completely consistent, esp for debian-multimedia
  169. # He we store manual mappings for these..
  170. def addOrphanPkgs(pkg2src):
  171. pkg2src['liblame-dev'] = "lame";
  172. pkg2src['lame-extras'] = "lame";
  173. pkg2src['moonlight'] = "moon";
  174. pkg2src['libmoon0'] = "moon";
  175. pkg2src['xmms-mp4'] = "xmms2";
  176. pkg2src['xmms-mp4'] = "xmms2";
  177. pkg2src['lazarus-src-0.9.30'] = "lazarus";
  178. pkg2src['lazarus-ide-0.9.30'] = "lazarus";
  179. pkg2src['lcl-qt4-0.9.30'] = "lazarus";
  180. pkg2src['lazarus-ide-qt4-0.9.30'] = "lazarus";
  181. pkg2src['lcl-gtk2-0.9.30'] = "lazarus";
  182. pkg2src['lazarus-ide-gtk2-0.9.30'] = "lazarus";
  183. pkg2src['lcl-units-0.9.30'] = "lazarus";
  184. pkg2src['lazarus-0.9.30'] = "lazarus";
  185. pkg2src['lazarus-doc-0.9.30'] = "lazarus";
  186. pkg2src['lcl-0.9.30'] = "lazarus";
  187. pkg2src['lcl-utils-0.9.30'] = "lazarus";
  188. pkg2src['lcl-nogui-0.9.30'] = "lazarus";
  189. pkg2src['libx264-65'] = "x264";
  190. pkg2src['libx264-114'] = "x264";
  191. pkg2src['libx264-60'] = "x264";
  192. # pkg2src['libmlt3']
  193. # pkg2src['libgmerlin-avdec0']
  194. # pkg2src['libxul-dev']
  195. # pkg2src['libmyth-0.23.1-0']
  196. # pkg2src['libmpeg3hv']
  197. # pkg2src['libquicktimehv']
  198. # pkg2src['libxul0d']
  199. # pkg2src['acroread-fonts-kor']
  200. ###############################################################################
  201. ## Parse dpkg Packages file, create map deb-name->pkg-name
  202. def parsePackages(pkgfile):
  203. mydir = cache = config['DIR']['cache_dir']
  204. deb2pkg = dict()
  205. pkg2virt = dict()
  206. virt2pkg = ()
  207. logging.info('Parsing Packages file...\n')
  208. pkgfile = mydir + pkgfile
  209. #TODO open and parse pkg file
  210. ###############################################################################
  211. ## Parse dpkg Sources file, create map pkg-name->src-name
  212. def parseSources(srcfile):
  213. mydir = cache = config['DIR']['cache_dir']
  214. checklinecont = 0
  215. pkg2src = dict()
  216. logging.info('Parsing Sources file...\n')
  217. srcfile = mydir + srcfile
  218. #TODO open and parse sources file
  219. ###############################################################################
  220. def getSHA1(myhash, collection):
  221. return collection.find({"hash": myhash})
  222. ###############################################################################
  223. def addSHA1(myhash, deb, src):
  224. dic = getSHA1(myhash)
  225. thash = dic["hash"]
  226. tdeb = dic["deb"]
  227. tsrc = dic["src"]
  228. #TODO insert SHA to database
  229. ###############################################################################
  230. ## Parse Sha1Sums file. Format: "sha1sum::deb-name::unix-file-path"
  231. ## Create 2 maps: sha1sum->file, file->deb-name
  232. def parseSha1Sums(sha1file):
  233. pass
  234. ###############################################################################
  235. ## Parse local dpkg status, return list of debs
  236. def parseStatus(stsfile):
  237. pass
  238. ###############################################################################
  239. ## Parse Advisory (only Debian supported atm
  240. def parseAdvisory(adv):
  241. if state['vendor'] == 'debian':
  242. return da.parseDSAhtml(adv)
  243. else:
  244. print('Unsupported distribution. We only support Debian at the moment')
  245. system.exit(1)
  246. ###############################################################################
  247. ## Manually fix problems with Advisory entries
  248. def fixAdvisoryQuirks(arg, state, dsastats):
  249. if state['vendor'] == 'debian':
  250. return da.fixDSAquirks(arg, dsastats)
  251. else:
  252. print('Unsupported distribution. We only support Debian at the moment')
  253. system.exit(1)
  254. ###############################################################################
  255. ## Extract CVE ids from new advisories and print URL for mirror script
  256. def printCVEs(myid,adv, state):
  257. logging.info('Looking for CVEs in advisory...\n')
  258. dsastats = parseAdvisory(adv)
  259. if dsastats == []:
  260. return
  261. ## fix DSAs that don't contain correct CVE refs
  262. dsastats = fixAdvisoryQuirks(myid, state, dsastats);
  263. #TODO Fix this part
  264. ##for cve_id in dsastats
  265. ###############################################################################
  266. ## Update internal vuln. DB with new Advisory info
  267. ## Creates CVEtable for MTBF computation:
  268. ## ( cve-id => (date, delay, score1, score2, score3))
  269. def updateCVETables(myid, dsatable, state, src2dsa, dsa2cve, cvetable, client):
  270. logging.info('Updating vulnerability database with advisory ' + state['vendor'] + str(myid) + ' \n')
  271. adv = dsatable[myid]
  272. dsastats = parseAdvisory(adv)
  273. if dsastats == []:
  274. return
  275. dsastats = fixAdvisoryQuirks(myid, state, dsastats)
  276. for srcpkg in dsastats[0]:
  277. if srcpkg in src2dsa:
  278. src2dsa[srcpkg].append(myid)
  279. else:
  280. src2dsa[srcpkg] = []
  281. src2dsa[srcpkg].append(myid)
  282. dsa2cve[str(myid)] = dsastats[2]
  283. for cve_id in dsastats[2]:
  284. # No fetch CVE We use mongodb and cve-search
  285. cve = cv.fetchCVE(cve_id, client)
  286. cvestats = cv.parseCVE(cve_id, cve)
  287. # print(cvestats)
  288. # print(dsastats)
  289. finaldate = cvestats[0]
  290. if cvestats[0] > dsastats[1] or cvestats[0] == 0:
  291. finaldate = dsastats[1]
  292. cvedata = (finaldate, dsastats[1]-finaldate, cvestats[1], cvestats[2], cvestats[3])
  293. ## print(cvedata)
  294. cvetable[cve_id] = cvedata
  295. return cvetable
  296. ###############################################################################
  297. ## Check for updates on Package information
  298. def aptsec_update(state, config, dsatable, client, src2dsa, dsa2cve, cvetable):
  299. args = sys.argv
  300. # if not('--offline' in args):
  301. # fetchMeta('Packages')
  302. # fetchMeta('Sources')
  303. # fetchMeta('Sha1Sums')
  304. now = datetime.datetime.now()
  305. if not('--cves' in args):
  306. parsePackages('Packages')
  307. parseSources('Sources')
  308. # if not('--nosha1' in args):
  309. # parseSha1sums('Sha1Sums')
  310. if state['vendor'] == 'debian':
  311. newAdv = da.checkDSAs(state, config)
  312. else:
  313. print('Unsupported distribution. We only support Debian at the moment')
  314. system.exit(1)
  315. for myid in newAdv:
  316. if myid in dsatable:
  317. logging.info(state['vendor'] + ' advisory ' + myid + ' already known.\n')
  318. elif '--cves' in args:
  319. ## scan for CVE urls only?
  320. printCVEs(myid, newAdv[myid])
  321. else:
  322. ## store advisory and parse it
  323. dsatable[myid] = newAdv[myid]
  324. updateCVETables(myid, dsatable, state, src2dsa, dsa2cve, cvetable, client)
  325. # recompute all pkg statistics
  326. for srcpkg in src2dsa:
  327. processCVEs(srcpkg, now, src2dsa, dsa2cve, cvetable, config)
  328. return 0
  329. ###############################################################################
  330. ## find list of src pkgs from bin pkgs based on pkg2src
  331. def resolvePkg2Src(pkglist, pkg2src):
  332. srclist = []
  333. for pkg in pkglist:
  334. if pkg in pkg2src:
  335. srcpkg = pkg2src[pkg]
  336. srclist.append(srcpkg)
  337. else:
  338. logging.info('Could not find source package for: ' + pkg + ' .\n')
  339. return srclist
  340. ###############################################################################
  341. ## compute and store MTBF, MTBR and Scores of each src pkg
  342. ## output: %src2mtbf:
  343. ## (srcpkg=> ())
  344. def processCVEs(pkg, now, src2dsa, dsa2cve, cvetable, config):
  345. stats = [now, 0, 0, 0, 0, 0, 0]
  346. mylambda = config['TRUST']['lambda']
  347. cvestats = dict()
  348. logging.info('Processing package: ' + pkg + '.\n')
  349. # print(dsa2cve)
  350. ## @cvestats = (date base-score impact-score exploit-score)
  351. for dsa_id in src2dsa[pkg]:
  352. try:
  353. for cve_id in dsa2cve[dsa_id]:
  354. tt = cvetable[cve_id][0]
  355. if tt in cvestats:
  356. cvestats[cvetable[cve_id][0]] += 1
  357. else:
  358. cvestats[cvetable[cve_id][0]] = 1
  359. stats[1] += 1
  360. except KeyError:
  361. for cve_id in dsa2cve[str(dsa_id)]:
  362. tt = cvetable[cve_id][0]
  363. if tt in cvestats:
  364. cvestats[cvetable[cve_id][0]] += 1
  365. else:
  366. cvestats[cvetable[cve_id][0]] = 1
  367. stats[1] += 1
  368. # Ignore pkgs with less than one incident, should not happen..
  369. if stats[1] < 1:
  370. return
  371. prev_date = 0
  372. weight = 0
  373. dates = sorted(cvestats, key = cvestats.get)
  374. stats[0] = dates[0]
  375. count = sum(cvestats.values())
  376. print(pkg + ' ' + str(count))
  377. if pkg == 'linux-2.6':
  378. # print(src2dsa[pkg])
  379. pkg_plot(pkg, cvestats)
  380. for date in dates:
  381. pass
  382. ## Need to do compute value
  383. ##TODO Code to compute trust goes here
  384. ###############################################################################
  385. ## plot vulnerability time distribution for a single package
  386. def pkg_plot(pkg, cvestats):
  387. colors = list("rgbcmyk")
  388. items = list(cvestats.items())
  389. print(items)
  390. items.sort(key=lambda tup: tup[0])
  391. x = []
  392. y = []
  393. for data_dict in items:
  394. x.append(parser.parse(data_dict[0]))
  395. y.append(data_dict[1])
  396. plt.plot_date(x, y)
  397. # plt.legend(data_dict.keys())
  398. plt.show()
  399. return 0
  400. ###############################################################################
  401. ## print some meta-info on internal data
  402. def aptsec_about(dsatable, cvetable, pkg2src, src2dsa):
  403. num_dsa = len(dsatable)
  404. num_cve = len(cvetable)
  405. num_pkg = len(pkg2src)
  406. num_src = len(src2dsa)
  407. print('\nThe current database records %d binary packages and %d DSAs.\n', num_pkg, num_src)
  408. print('%d CVEs are assiciated with %d source packages.\n', num_cve, num_src)
  409. ###############################################################################
  410. ## use scores to suggest alternative packages
  411. def aptsec_alternatives(pkg):
  412. pass
  413. ###############################################################################
  414. ## print overview for pkg high scores
  415. def aptsec_hitlist():
  416. pass
  417. ###############################################################################
  418. ## evaluation helper
  419. ## compute stats until date given in $2, then compute stats
  420. ## for the next year to check accuracy of the prediction.
  421. ## @cvestats = (date base-score impact-score exploit-score)
  422. def simulate_stats(pkg, year):
  423. pass
  424. ###############################################################################
  425. ##TODO Printing functions
  426. ###############################################################################
  427. ## show info on a single src pkg, resolv to src if needed
  428. def aptsec_show(pkg, state, pkg2src, src2dsa, src2mtbf, cvetable):
  429. if state['vendor'] == 'debian':
  430. ADV = 'DSA-'
  431. else:
  432. print('Unsupported distribution. We only support Debian at the moment')
  433. system.exit(1)
  434. if (not(pkg in src2dsa)) and (pkg in pkg2src):
  435. print('\nResolving ' + pkg + ' to ' + pkg2src[pkg] + '\n')
  436. pkg = pkg2src[pkg]
  437. print('\nThe following binary packages are created from ' + pkg + ' :\n\n')
  438. lines = 0
  439. for i in pkg2src:
  440. if pkg2src[i] == pkg:
  441. print(i + '\n')
  442. lines += 1
  443. if lines < 1:
  444. print('-\n')
  445. if not (pkg in src2dsa and pkg in src2mtbf):
  446. print('\nNo vulnerabilities recorded for source package ' + pkg + '.\n')
  447. return
  448. print('\nAdvisories on package ' + pkg + ':\n\n')
  449. for dsa_id in sorted(src2dsa[pkg], key = src2dsa[pkg].get):
  450. print(ADV + dsa_id + '\n')
  451. for cve_id in dsa2cve[dsa_id]:
  452. (sec, minut, hrs, day, mon, yr) = gmtime(cvetable[cve_id][0])
  453. print('%s: Base Score: %04.1f, %02d.%02d.%04d\n', cve_id, cvetable[cve_id][2], day, mon+1, yr+1900)
  454. stats = src2mtbf[pkg]
  455. (sec, minut, hrs, day, mon, yr) = gmtime(stats[0])
  456. print('Now we print various iformation \n')
  457. ###############################################################################
  458. ## print help text
  459. def aptsec_help():
  460. print('See manual for correct usage\n')
  461. ###############################################################################
  462. ## Print system status report from component(files) measurements (sha1sums)
  463. ## Expected input format is Linux IMA. We assume input was validated.
  464. ##
  465. ## Note: aptsec_status(), considers *reportedly installed* packages, while this
  466. ## one looks at *actually loaded* software that influenced the CPU since bootup.
  467. def aptsec_attest(sha1file):
  468. pass
  469. ## Main Program starts here!!
  470. try:
  471. action = sys.argv[1]
  472. except IndexError:
  473. # print('No argument given')
  474. # aptsec_help()
  475. # sys.exit(0)
  476. action = ''
  477. client = MongoClient()
  478. dsatable = dict()
  479. cve_db = client.cvedb
  480. src2dsa = dict()
  481. dsa2cve = dict()
  482. cvetable = dict()
  483. (state, err) = load_state()
  484. state['vendor'] = 'debian'
  485. #detect_distribution()
  486. #d = state['cache_dir']
  487. #if not os.path.exists(d):
  488. # os.makedirs(d)
  489. if action == 'update':
  490. (dsatable, src2dsa, dsa2cve, cvetable) = load_DBs()
  491. # loadsha1lists()
  492. aptsec_update(state,config, dsatable, client, src2dsa, dsa2cve, cvetable)
  493. # save_sha1lists()
  494. save_DBs(dsatable, src2dsa, dsa2cve, cvetable)
  495. save_state(state)
  496. elif action == 'status':
  497. load_DBs or exit(1)
  498. #handle errors more gracefully
  499. aptsec_status(sys.argv[2])
  500. elif action == 'show':
  501. load_DBs or exit(1)
  502. #handle errors more gracefully
  503. aptsec_show(sys.argv[2])
  504. else:
  505. aptsec_help()
  506. #print(state)
  507. save_state(state)
  508. #cve_db = client.cvedb
  509. #collection = db.cves
  510. #testcvss = collection.find_one({"cvss": 9.3})
  511. #print(testcvssi