| 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. 87. 88. 89. 90. 91. 92. 93. 94. 95. 96. 97. 98. 99. 100. 101. 102. 103. 104. 105. 106. 107. 108. 109. 110. 111. 112. 113. 114. 115. 116. 117. 118. 119. 120. 121. 122. 123. 124. 125. 126. 127. 128. 129. 130. 131. 132. 133. 134. 135. 136. 137. 138. 139. 140. 141. 142. 143. 144. 145. 146. 147. 148. 149. 150. 151. 152. 153. 154. 155. 156. 157. 158. 159. 160. 161. 162. 163. 164. 165. 166. 167. 168. 169. 170. 171. 172. 173. 174. 175. 176. 177. 178. 179. 180. 181. 182. 183. 184. 185. 186. 187. 188. 189. 190. 191. 192. 193. 194. 195. 196. 197. 198. 199. 200. 201. 202. 203. 204. 205. 206. 207. 208. 209. 210. 211. 212. 213. 214. 215. 216. 217. 218. 219. 220. 221. 222. 223. 224. 225. 226. 227. 228. 229. 230. 231. 232. 233. 234. 235. 236. 237. 238. 239. 240. 241. 242. 243. 244. 245. 246. 247. 248. 249. 250. 251. 252. 253. 254. 255. 256. 257. 258. 259. 260. 261. 262. 263. 264. 265. 266. 267. 268. 269. 270. 271. 272. 273. 274. 275. 276. 277. 278. 279. 280. 281. 282. 283. 284. 285. 286. 287. 288. 289. 290. 291. 292. 293. 294. 295. 296. 297. 298. 299. 300. 301. 302. 303. 304. 305. 306. 307. 308. 309. 310. 311. 312. 313. 314. 315. 316. 317. 318. 319. 320. 321. 322. 323. 324. 325. 326. 327. 328. 329. 330. 331. 332. 333. 334. 335. 336. 337. 338. 339. 340. 341. 342. 343. 344. 345. 346. 347. 348. 349. 350. 351. 352. 353. 354. 355. 356. 357. 358. 359. 360. 361. 362. 363. 364. 365. 366. 367. 368. 369. 370. 371. 372. 373. 374. 375. 376. 377. 378. 379. 380. 381. 382. 383. 384. 385. 386. 387. 388. 389. 390. 391. 392. 393. 394. 395. 396. 397. 398. 399. 400. 401. 402. 403. 404. 405. 406. 407. 408. 409. 410. 411. 412. 413. 414. 415. 416. 417. 418. 419. 420. 421. 422. 423. 424. 425. 426. 427. 428. 429. 430. 431. 432. 433. 434. 435. 436. 437. 438. 439. 440. 441. 442. 443. 444. 445. 446. 447. 448. 449. 450. 451. 452. 453. 454. 455. 456. 457. 458. 459. 460. 461. 462. 463. 464. 465. 466. 467. 468. 469. 470. 471. 472. 473. 474. 475. 476. 477. 478. 479. 480. 481. 482. 483. 484. 485. 486. 487. 488. 489. 490. 491. 492. 493. 494. 495. 496. 497. 498. 499. 500. 501. 502. 503. 504. 505. 506. 507. 508. 509. 510. 511. 512. 513. 514. 515. 516. 517. 518. 519. 520. 521. 522. 523. 524. 525. 526. 527. 528. 529. 530. 531. 532. 533. 534. 535. 536. 537. 538. 539. 540. 541. 542. 543. 544. 545. 546. 547. 548. 549. 550. 551. 552. 553. 554. 555. 556. 557. 558. 559. 560. 561. 562. 563. 564. 565. 566. 567. 568. 569. 570. 571. 572. 573. 574. 575. 576. 577. 578. 579. 580. 581. 582. 583. 584. 585. 586. 587. 588. 589. 590. 591. 592. 593. 594. 595. 596. 597. 598. 599. 600. 601. 602. 603. 604. 605. 606. 607. 608. 609. 610. 611. 612. 613. 614. 615. 616. 617. 618. 619. 620. 621. 622. 623. 624. 625. 626. 627. 628. 629. 630. 631. 632. 633. 634. 635. 636. 637. 638. 639. 640. 641. 642. 643. 644. 645. 646. 647. 648. 649. 650. 651. 652. 653. 654. 655. 656. 657. 658. 659. 660. 661. 662. 663. 664. 665. 666. 667. 668. 669. 670. 671. 672. 673. 674. 675. 676. 677. 678. 679. 680. 681. 682. 683. 684. 685. 686. 687. 688. 689. 690. 691. 692. 693. 694. 695. 696. 697. 698. 699. 700. 701. 702. 703. 704. 705. 706. 707. 708. 709. 710. 711. 712. 713. 714. 715. 716. 717. 718. 719. 720. 721. 722. 723. 724. 725. 726. 727. 728. 729. 730. 731. 732. 733. 734. 735. 736. 737. 738. 739. 740. 741. 742. 743. 744. 745. 746. 747. 748. 749. 750. 751. 752. 753. 754. 755. 756. 757. 758. 759. 760. 761. 762. 763. 764. 765. 766. 767. 768. 769. 770. 771. 772. 773. 774. 775. 776. 777. 778. 779. 780. 781. 782. 783. 784. 785. 786. 787. 788. 789. 790. 791. 792. 793. 794. 795. 796. 797. 798. 799. 800. 801. 802. 803. 804. 805. 806. 807. 808. 809. 810. 811. 812. 813. 814. 815. 816. 817. 818. 819. 820. 821. 822. 823. 824. 825. 826. 827. 828. 829. 830. 831. 832. 833. 834. 835. 836. 837. 838. 839. 840. 841. 842. 843. 844. 845. 846. 847. 848. 849. 850. 851. 852. 853. 854. 855. 856. 857. 858. 859. 860. 861. 862. 863. 864. 865. 866. 867. 868. 869. 870. 871. 872. 873. 874. 875. 876. 877. 878. 879. 880. 881. 882. 883. 884. 885. 886. 887. 888. 889. 890. 891. 892. 893. 894. 895. 896. 897. 898. 899. 900. 901. 902. 903. 904. 905. 906. 907. 908. 909. 910. 911. 912. 913. 914. 915. 916. 917. 918. 919. 920. 921. 922. 923. 924. 925. 926. 927. 928. 929. 930. 931. 932. 933. 934. 935. 936. 937. 938. 939. 940. 941. 942. 943. 944. 945. 946. 947. 948. 949. 950. 951. 952. 953. 954. 955. 956. 957. 958. 959. 960. 961. 962. 963. 964. 965. 966. 967. 968. 969. 970. 971. 972. 973. 974. 975. 976. 977. 978. 979. 980. 981. 982. 983. 984. 985. 986. 987. 988. 989. 990. 991. 992. 993. 994. 995. 996. 997. 998. 999. 1000. 1001. 1002. 1003. 1004. 1005. 1006. 1007. 1008. 1009. 1010. 1011. 1012. 1013. 1014. 1015. 1016. 1017. 1018. 1019. 1020. 1021. 1022. 1023. 1024. 1025. 1026. 1027. 1028. 1029. 1030. 1031. 1032. 1033. 1034. 1035. 1036. 1037. 1038. 1039. 1040. 1041. 1042. 1043. 1044. 1045. 1046. 1047. 1048. 1049. 1050. 1051. 1052. 1053. 1054. 1055. 1056. 1057. 1058. 1059. 1060. 1061. 1062. 1063. 1064. 1065. 1066. 1067. 1068. 1069. 1070. 1071. 1072. 1073. 1074. 1075. 1076. 1077. 1078. 1079. 1080. 1081. 1082. 1083. 1084. 1085. 1086. 1087. 1088. 1089. 1090. 1091. 1092. 1093. 1094. 1095. 1096. 1097. | #!/usr/bin/env python # -*- coding: iso-8859-1 -*- """ pide.py pide (Python Intrusion Detection Enviroment) is an intrusion detection system for checking the integrity of files. Basically a python implemenation of tripwire/aide. Can read AIDE/Tripwire config files, but does not make AIDE/Tripwire compatible databases.. Implements most of the functionality of AIDE. Copyright (C) 2004 - Lars Strand <lars strand at gnist org> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. http://www.gnu.org/copyleft/gpl.html TODO: * xml data format * crypt database --> problem with 3rd party modules... http://rogue.amk.ca/pipermail/pct/2003-July/000022.html Changelog: DONE --> warn dead symlinks! DONE --> stats on directories as well DONE --> +S (ok if size is the same or growing) --> gives a small preformance penalty, since more cheks has to be added DONE --> access by name! not in tuple http://mail.python.org/pipermail/patches/2001-December/006887.html DONE --> add group of files - added globbing DONE --> compressed database DONE --> +b (add number of blocks) DONE --> if rules is only letters (the same as +) - REVERTED!! DONE --> rule-options ==> binLib-p-sha1 DONE --> degree of summary: short, normal, detail DONE --> +sha1 or -sha1 also is interpreted as +a etc.. attributes MUST have + or - in front.. DONE --> add more template rules DONE --> verbose setting DONE --> check for save new db and old db BEFORE doing any stats! SHOULD BE OK NOW - done some sanity checks on =, $ and *! DONE --> version control (must have at least python ver. 2.3) """ # Package definitions. __program__ = 'PIDE' __version__ = '0.6' __date__ = '2004/22/03' __author__ = 'Lars Strand <lars at unik no>' __licence__ = 'GPL' __copyright__ = 'Copyright (C) 2004 Lars Strand' import getopt, sys, os, os.path, re, glob, stat, md5, sha, tempfile, time, gzip, string ########################## makeSysDB def makeSysDB(buildpaths, confvar): """We get a dictionary of 'file => att' and a list of attributes. for each file: - make stats according to 'att' - save stats to db return db """ sysDB = {} # for each file for file in buildpaths: attributes = buildpaths[file] if verbose: print "stat:", file, # The stats datastructure: # filename => [stat1, stat2, stat3, .....], filename2 => [stat1. stat2...] # file = [perm, inode, lcount, uid, gid, size, blocks, atime, mtime, ctime, md5, sha1] sysDB[file] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] # stat the file, print warning if dead symlink if os.path.islink(file) and confvar['warn_dead_symlinks'] == 'yes': try: filemode = os.stat(file) except: # the stat will fail if dead symlink print "WARNING: Dead symlink? %s" % file else: try: filemode = os.stat(file) except: print "WARNING: Unable to stat file '%s'" % file # extract and put the stat in the right place # NB! if the rule contain both + and -, the + has presedence # +S (check for growing size) is not here, must do a check # of this size and the old one. # The rest of the stats if '+p' in attributes: # permission and file mode bits if verbose: print "+p", sysDB[file][0] = filemode.st_mode if '+i' in attributes: # inode number if verbose: print "+i", sysDB[file][1] = filemode.st_ino if '+n' in attributes: # number of (hard) links if verbose: print "+n", sysDB[file][2] = filemode.st_nlink if '+u' in attributes: # user id of owner if verbose: print "+u", sysDB[file][3] = filemode.st_uid if '+g' in attributes: # group id of owner if verbose: print "+g", sysDB[file][4] = filemode.st_gid if '+s' or '+S' in attributes: # size of file if verbose and '+s' in attributes: print "+s", if verbose and '+S' in attributes: print "+S", sysDB[file][5] = filemode.st_size if '+b' in attributes: # number of blocks if verbose: print "+b", sysDB[file][6] = filemode.st_blocks if '+a' in attributes: # access timestamp if verbose: print "+a", sysDB[file][7] = filemode.st_atime if '+m' in attributes: # modification timestamp if verbose: print "+m", sysDB[file][8] = filemode.st_mtime if '+c' in attributes: # inode creation timestamp if verbose: print "+c", sysDB[file][9] = filemode.st_ctime if os.path.isfile(file): if '+md5' in attributes: # MD5 signature if verbose: print "+md5", # open the file try: f = open(file, 'rb') except: print "Unable to open %s for MD5 hash" % file continue hash = md5.new() # read the file in chuncks while 1: r = f.read(8096) if not r: break hash.update(r) # feed the hash object f.close() sysDB[file][10] = hash.hexdigest() if os.path.isfile(file): if '+sha1' in attributes: # SHA1 signature if verbose: print "+sha1", # open the file try: f = open(file, 'rb') except: print "Unable to open %s for SHA-1 hash" % file continue hash = sha.new() # read the file in chuncks while 1: r = f.read(8096) if not r: break hash.update(r) # feed the hash object f.close() sysDB[file][11] = hash.hexdigest() # some space if verbose: print "" # debug #import pprint #pprint.pprint(sysDB) return sysDB ########################## buildPaths def buildPaths(rcfile): """Read the config file: * read all config variables (if any) * read all rules (if any give) - a set of default rules listed * read files (os.walk) Returns a 'files' (dict) of 'files => attributs' and confvar (dict) of 'variablename => value'""" # read the config file try: ifile = open(rcfile, 'r') except: usage("Unable to open config file: %s" + ifile) sys.exit(1) # read the config file # grep '#' lines comment = re.compile(r"\s*\#.*") # blank lines blanks = re.compile(r"^$") # database dbfile = r"\s*database=file:(\S+)" dbfilenew = r"\s*database_out=file:(\S+)" dbzip = r"\s*gzip_dbout=(\S+)" dbcrypt = r"\s*crypt_dbout=(\S+)" dbcryptkey = r"\s*crypt_key=(\S+)" dbwarnsym = r"\s*warn_dead_symlinks=(\S+)" # crypt add - public key or password? # length of summary summary = r"\s*summary=(\S+)" # dictionary to hold optional variables # fill inn some default variables confvar = { 'database' : '/var/db/pide/pide.db', 'database_out' : '/var/db/pide/pide.db.new', 'gzip_dbout' : 'yes', 'crypt_dbout' : 'no', 'warn_dead_symlinks' : 'no', 'summary' : 'normal' } #print "POP!" # all defined enviroment variables get exported (for us with cron) # @@define MAILTO [email protected] envvar = re.compile(r"\s*@@define\s+(\S+)\s+(\S+)") # undef enviroment variables # @@undef VAR uenvvar = re.compile(r"\s*@@undef\s+(\S+)") # custom rules: # Binlib = +p+i+n+u+g+s+b+m+c+md5+sha1 rule = re.compile(r"\s*(\S+)\s*=\s*(.+)") # p : permission and file mode bits a: access timestamp # i : inode number m: modification timestamp # n : number of links (ref count) c: inode creation timestamp # u : user id of owner md5: MD5 signature # g : group id of owner tiger: tiger signature (not implemented) # s : size of file rmd160: RMD160 signature (not implemented) # b : number of blocks (Linux) sha1: SHA1 signature # S : check for growing size # # Predefined template rules: # Note!! '+' (plus) has presedence over '-' (minus), so the rule +p-p gives +p! # Note2! You MUST have assign +/- (plus/minus) in front attributes! If not the # attribute will be ignored! # # R : [R]ead-only (+p+i+n+u+g+s+b+m+md5+tiger+rmd160+sha1-a) # L : [L]og file (+p+i+n+u+g-s-b-a-m-md5-tiger-rmd160-sha1) # N : ignore [N]othing (+p+i+n+u+s+b+g+s+a+m+c+md5+tiger+rmd160+sha1) # E : ignore [E]verything (-p-i-n-u-s-b-g-s-a-m-c-md5-tiger-rmd160-sha1) # > or Logs : growing logfile (+p+u+g+i+n+S) # Binlib : binary files (+p+i+n+u+g+s+b+m+c+md5+sha1) # ConfFiles : config files (+p+i+n+u+g+s+b+m+c+md5+sha1) # Devices : device files /dev (+p+i+n+u+g+s+b+c) # Databases : databases (+p+n+u+g) # StaticDir : static directories (+p+i+n+u+g) # ManPages : manual pages (+p+i+n+u+g+s+b+m+c+md5+sha1) # note: rules = { 'R' : '+p+i+n+u+g+s+b+m-a+md5+tiger+rmd160+sha1-a', 'L' : '+p+i+n+u+g-s-b-a-m-md5-tiger-rmd160-sha1', 'N' : '+p+i+n+u+s+g+s+b+a+m+c+md5+tiger+rmd160+sha1', 'E' : '-p-i-n-u-s-g-s-b-a-m-c-md5-tiger-rmd160-sha1', '>' : '+p+u+g+i+n+S', 'Logs' : '+p+u+g+i+n+S', 'Binlib' : '+p+i+n+u+g+s+b+m+c+md5+sha1', 'ConfFiles' : '+p+i+n+u+g+s+b+m+c+md5+sha1', 'Devices' : '+p+i+n+u+g+s+b+c', 'Databases' : '+p+n+u+g', 'StaticDir' : '+p+i+n+u+g', 'ManPages' : '+p+i+n+u+g+s+b+m+c+md5+sha1' } # '!' signifies the entry is to be pruned (inclusive) from # the list of files to be scanned. # !/root/.bash_history notfile = re.compile(r"\s*!(\S+)") notfiles = [] # files/directories which are pruned # The files/directories # =/boot$ Binlib # /bin binlib file = re.compile(r"\s*(\S+)\s+(\S+)") files = {} linenumber = 0 # for each line in config file if verbose: print "Reading config file, line by line" for line in ifile: linenumber += 1 # match here, so we can extract var later commentmatch = re.search(comment, line) blankmatch = re.search(blanks, line) dbfilematch = re.search(dbfile, line) dbfilenewmatch = re.search(dbfilenew, line) dbzipmatch = re.search(dbzip, line) dbwarnsymmatch = re.search(dbwarnsym, line) summarymatch = re.search(summary, line) envvarmatch = re.search(envvar, line) uenvvarmatch = re.search(uenvvar, line) rulesmatch = re.search(rule, line) notfilematch = re.search(notfile, line) filesmatch = re.search(file, line) # skip all comments and blank lines if commentmatch: continue if blankmatch: continue # where should the db be read from? elif dbfilematch: database = dbfilematch.group(1) confvar['database'] = database # where should the new db be saved? elif dbfilenewmatch: databaseout = dbfilenewmatch.group(1) confvar['database_out'] = databaseout # should db be zipped? elif dbzipmatch: dbgzip = dbzipmatch.group(1) if dbgzip == 'no': confvar['gzip_dbout'] = 'no' # warn on dead symlinks? elif dbwarnsymmatch: if dbwarnsymmatch.group(1) == 'yes': confvar['warn_dead_symlinks'] = 'yes' # length of summary: valid are 'short', 'normal' and 'detail' elif summarymatch: summaryout = summarymatch.group(1) if summaryout == 'short' or summaryout == 'normal' or summaryout == 'detail': confvar['summary'] = summaryout else: print "WARNING: unrecognized summary value '%s' on line: %s" % (summaryout, linenumber) # any evniroment variables? (for cron) elif envvarmatch: #print "envvarmatch" # typcial format: MAILTO, someone@localhost os.putenv(envvarmatch.group(1), envvarmatch.group(2)) # undef any enviromentvariables elif uenvvarmatch: os.unsetenv(uenvvarmatch.group(1)) # own defined rules elif rulesmatch: #print "rulematch" rules[rulesmatch.group(1)] = rulesmatch.group(2) # files to exclude - remove these files from files{} when loop complete elif notfilematch: #print "notfilematch" #print "NOTFILE: " + str(notfilematch.group(1)) # it the line ends with '$', remove it. # used to express the end of a path # /tem$ == mening /tem and NOT /temm or /temp or /te ... # but the glob function in python does that automatic, and the # glob function don't like '$', so we remove it if notfilematch.group(1)[-1:] == "$": for i in glob.glob(notfilematch.group(1)[:-1]): notfiles.append(i) else: for i in glob.glob(notfilematch.group(1)): notfiles.append(i) #print "notfiles: " + str(notfiles) # get files/directories elif filesmatch: # se comment for notfilematch - the same thing goes here if filesmatch.group(1)[-1:] == "$": file2 = filesmatch.group(1)[:-1] # do not include '$' at end #print file2 else: file2 = filesmatch.group(1) # has no '$' at end #print file2 # for hver fil, sjekk om group2 = en rulesmatch # s� legg til todo = filesmatch.group(2) #print "file: " + file2 #print "todo: " + todo #print "rules: " + str(rules) # if using a template (R/L/N/E/>), translate to the # corresponding "rules" if rules.has_key(todo): todo = rules[todo] # NB! Rules may also be used as # BinLib-md5-sha1 # Where 'BinLib' is a rule and we don't want md5 and sha1 # The above test will fail, and we need to split the 'todo' # with '+' and/or '-' as delimiter else: # example: 'BinLib-p-a-c+md5 -s' # first we must get 'BinLib' - fetch that using regexp r = re.search(r"^(\w+)(.*$)" ,todo) # matches (text+numbers)(the rest) todoo = "" # to hold the translated rule if r: # if match if rules.has_key(r.group(1)): # is it a know rule? todoo = rules[r.group(1)] # translate rule for later user else: # unknow rule! print "WARNING: Unknown rule on line %d! Ignoring" % linenumber continue # ok we now have translated the rule, add the rest if any #print "GROUP: ",r.group(2) if r.group(2): # more than just a rule? rulessplit = string.split(str(r.group(2))) # first we split on blanks for g in rulessplit: # add the rest todoo += g # NB! The rule may now contain both -p and +p # but when doing stats, the +p has presedence # add rule: todo = todoo # '=' signifies the entry is to be added, but if it's # a directory, then all its contents are pruned (useful for /tmp) # No problem with glob. if verbose: print "Building pathlist: %s" % file2 if file2[0] == '=': for f in glob.glob(file2[1:]): files[f] = todo # do not include '=' #print "FILES DIR: ", files # ok, add the file/directory else: for f in glob.glob(file2): if os.path.isfile(f): # if file - just add files[os.path.join(f)] = todo elif os.path.isdir(f) or os.path.islink(f): # if dir - do a walk for root, dirs, file3 in os.walk(f): for i in file3: # for all files in dir files[os.path.join(root, i)] = todo if root: # add directory as well files[root] = todo else: print "WARNING: unrecognized line: %s" % line # exclude the files from notfiles if verbose: print "Excluding files, if any.." for i in notfiles: if files.has_key(i): del files[i] #import pprint #print "###########" #pprint.pprint(files) #print "filelist: " + str(files) #print "rules : " + str(rules) #print "END" # export all # Ok, we have dictionary of files in 'files' and a list of # variables in 'confvar' return files, confvar ########################## saveDB def saveDB(sysDB, confvar): """Save the database to file, given in 'database_out'. Compress the database if compress is true""" # where should the database be saved? # there is always a (default) value here dboutfilename = confvar['database_out'] # should we compress the database? (default = yes) if confvar.has_key('gzip_dbout'): if confvar['gzip_dbout'] == 'yes': # We should gzip the database try: # open the dbout file dbout = gzip.GzipFile(dboutfilename, 'w') except: usage("Unable to write to databasefile file: %s" % dboutfilename) sys.exit(1) else: # open the dbout file try: dbout = open(dboutfilename, 'w') except: usage("Unable to write to databasefile file: %s" % dboutfilename) sys.exit(1) # write to file dbout.write("# This file was generated by %s, version %s\n" % (__program__, __version__)) dbout.write("# Time of generation: %s\n" % time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())) dbout.write("# @@db_spec name perm inode size blocks lcount uid gid size atime mtime ctime md5 sha1\n") # write all the stats to file: for file in sysDB: # for each file in dictionary filestats = file # contruct a string for stat in sysDB[file]: # extract all stats.. filestats += ' ' + str(stat) # ..and append to string dbout.write(filestats+'\n') # write to file # close the file dbout.close() print "Database saved in %s" % dboutfilename ########################## readSysDB def readSysDB(confvar): """Read a pide database from file and put into a dictionary: {file : [stat1, stat2, stat3....], file2......} return the dictionary """ dbinfilename = confvar['database'] # if gzip_dbout == yes, we assume the db file is compressed if confvar['gzip_dbout'] == 'yes': # open using gzip try: dbin = gzip.GzipFile(dbinfilename, 'r') except: usage("Unable to read database file: %s" % dbinfilename) sys.exit(1) else: try: dbin = open(dbinfilename, 'r') except: usage("Unable to read database file: %s" % dbinfilename) sys.exit(1) # This is where we put our database fileDB = {} linenumber = 0 # grep '#' lines comment = re.compile(r"\s*\#.*") # blank lines blanks = re.compile(r"^$") # read file line by line if verbose: print "Reading database from disk, line by line" while 1: linenumber += 1 # in case of an error, print the line number line = dbin.readline() if not line: break # process line if re.search(comment, line): # skip comment lines continue elif re.search(blanks, line): # skip blank lines continue # split the line linesplit = string.split(line) # read arguments into a dictionary if len(linesplit) != 13: # each line has exactly 13 arguments! usage("Error occured reading database '%s' on line %d\nThe offending line was:\n%s\nCorrupt database?" % (dbinfilename, linenumber, line)) sys.exit(1) else: fileDB[linesplit[0]] = linesplit[1:] #import pprint #pprint.pprint(fileDB) return fileDB ########################## compareDB def compareDB(called, sysDB, fileDB, confvar, syspaths): """Should compare two databases: One (old) from disk, and the newly generated. The databases are two dictionaries. * sysDB = new generated database * fileDB = (old) database from disk """ missing = [] # files that are in old db, but not in new changed = [] # files that have changed since last check added = [] # new files not in old db #print "Compare" #import pprint #pprint.pprint(sysDB) for key in fileDB: try: # try in case there is no exceptions --> catch keyerrors if not sysDB[key]: # check if we have the file in the new one missing.append(key) # add missing file to list except: # keyerror, just.. continue # ..continue with next file if sysDB[key]: # file exists, check stats i = 0 # to iterate stats if verbose: print "Compare: %s" % key # for each stat in old db for stat in fileDB[key]: # compare each stat in old DB with the corresponding stat in new DB (sysDB) if str(stat) != str(sysDB[key][i]): # do we have a difference? # must do a exception on '+S' if i == 5 and '+S' in syspaths[key]: if int(fileDB[key][i]) > int(sysDB[key][i]): if verbose: print " Difference found!" # add the file, it it's not added already if not key in changed: if verbose: print " File added!" changed.append(key) # add changed file to list else: if verbose: print " Difference found!" # add the file, it it's not added already if not key in changed: if verbose: print " File added!" changed.append(key) # add changed file to list i += 1 # for each file NOT old db is considered 'new' files for key in sysDB: if not fileDB.has_key(key): added.append(key) #print "REMOVED: ", missing #print "CHANGED: ", changed #print "ADDED: ", added # if init and no changes - just return if called == "check" and not missing and not changed and not added: return else: # print summary - short/normal/detail get this summary print "\n%s found differences between database and filesystem!!" % __program__ print "\nTimestamp: %s" % time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) print "\nSummary:" print "Total number of files in database: %s" % len(fileDB) print "Total number of files on system : %s" % len(sysDB) print "Removed files : %s" % len(missing) print "New files : %s" % len(added) print "Changed files : %s" % len(changed) # normal/detail summary level if confvar['summary'] == 'normal' or confvar['summary'] == 'detail': # viewing the files that are missing/new/changed if called == "update": # When called as 'update'.. if len(missing) != 0: print "\nRemoved files:" # ...some syntax diff for file in missing: print " " + file if len(added) != 0: print "\nAdded files:" for file in added: print " " + file if called == "check": # When called as 'check'.. if len(missing) != 0: print "\nMissing files:" # .. some syntax diff for file in missing: print " " + file if len(added) != 0: print "\nNew files:" for file in added: print " " + file # changed files if len(changed) != 0: print "\nChanged files:" for file in changed: print " " + file # detail summary level if confvar['summary'] == 'detail': print "\nDetailed information about changes:" for file in changed: # for each changed file if os.path.isdir(file): print "Directory:", file else: print "File:", file i = 0 for stat in fileDB[file]: # iterate stats if str(stat) != str(sysDB[file][i]): # perm, inode, lcount, uid, gid, size, atime, mtime, ctime, md5, sha1 if i == 0: print " Perm : %-40s , %-40s" % (str(stat), str(sysDB[file][i])) elif i == 1: print " Inode : %-40s , %-40s" % (str(stat), str(sysDB[file][i])) elif i == 2: print " Lcount: %-40s , %-40s" % (str(stat), str(sysDB[file][i])) elif i == 3: print " Uid : %-40s , %-40s" % (str(stat), str(sysDB[file][i])) elif i == 4: print " Gid : %-40s , %-40s" % (str(stat), str(sysDB[file][i])) # must do some exceptions on +S if i == 5 and '+S' in syspaths[file] and int(fileDB[file][5]) > int(sysDB[file][5]): print " Size : %-40s , %-40s" % (str(stat), str(sysDB[file][i])) elif i == 5: print " Size : %-40s , %-40s" % (str(stat), str(sysDB[file][i])) # back to "normal" check if i == 6: print " Blocks: %-40s , %-40s" % (str(stat), str(sysDB[file][i])) elif i == 7: print " Atime : %-40s , %-40s" % (str(time.ctime(int(stat))), str(time.ctime(int(sysDB[file][i])))) elif i == 8: print " Mtime : %-40s , %-40s" % (str(time.ctime(int(stat))), str(time.ctime(int(sysDB[file][i])))) elif i == 9: print " Ctime : %-40s , %-40s" % (str(time.ctime(int(stat))), str(time.ctime(int(sysDB[file][i])))) elif i == 10: print " MD5 : %-40s , %-40s" % (str(stat), str(sysDB[file][i])) elif i == 11: print " SHA-1 : %-40s , %-40s" % (str(stat), str(sysDB[file][i])) i += 1 print "" # some space between each file ########################## integrityCheck def integrityCheck(init=0, check=0, update=0, rcfile="", verbose=0): """Starting point - call this if using as a module. * call 'buildPaths' which returns a list of files (with attributes) and a list of config variables * then call 'makeSysDB' to build a database of all files in 'files' * choice: - init: save db and quit - check: compare db on disk and the db generated by 'makeSysDB' print changes - update: compare db on disk and the db generated by 'makeSysDB' print updates - save new db on disk """ #print "init:", init #print "check:", check #print "update:", update # read the config file, returns a list of files in syspaths # and a list of config variables in 'confvar' if rcfile: if verbose: print "Parsing config file and building paths" syspaths, confvar = buildPaths(rcfile) else: usage("No config file defined") sys.exit(1) # Do some sanity checks before going into some heavy processing if init: # can we open db out for writing? If not there is no need to process files.. try: dbout = open(str(confvar['database_out']), 'w') dbout.close() except: usage("Unable to write to databasefile file: %s" % confvar['database_out']) sys.exit(1) if check or update: # can we open db for reading? If not we can't compare.. try: dbout = open(str(confvar['database']), 'r') dbout.close() except: usage("Unable to read to databasefile file: %s" % confvar['database']) sys.exit(1) # build database sysDB = database of current system # this can be quite large (several MB) if verbose: print "Building database of current system" sysDB = makeSysDB(syspaths, confvar) # init = save database as defined in 'database_out', gzip if specified if init: if verbose: print "Doing 'init' --> saving database" # save database to file saveDB(sysDB, confvar) # update = print the different between the new database (sysDB) and # the old one. Then save the new database in 'database' elif update: # read database from file if verbose: print "Reading database from file" fileDB = readSysDB(confvar) # compare the two databases if verbose: print "Compare database of system with (old) database on disk" compareDB("update", sysDB, fileDB, confvar, syspaths) # save the new database sysDB if verbose: print "Save database to file" saveDB(sysDB, confvar) # check = compare the new database 'sysDB' with the old one, # print the differenses elif check: # read database from file if verbose: print "Reading database from file" fileDB = readSysDB(confvar) # compare the two databases if verbose: print "Compare database of system with (old) database on disk" compareDB("check", sysDB, fileDB, confvar, syspaths) return 0 # end ########################## printconf def printconf(): """Print an example config file and exit""" print """# # pide.conf based on Tripwire's tw.config # # NOTE! the tiger and rmd160 checksum is NOT implemented! So it # it will be ignored (but md5 and sha1 should do the job). # # This file contains a list of files and directories that this # script will scan. Information collected from these files will be # stored in a database file. # # Format: [!|=] entry [attributes-flags] # # where: '!' signifies the entry is to be pruned (inclusive) from # the list of files to be scanned. # '=' signifies the entry is to be added, but if it is # a directory, then all its contents are pruned # (useful for /tmp). # # where: 'entry' is the absolute pathname of a file or a directory # # where 'attributes-flags' are in the format: # [template][ [+|-][pinugsam...] ... ] # # - : ignore the following atributes # + : do not ignore the following attributes # # p : permission and file mode bits a: access timestamp # i : inode number m: modification timestamp # n : number of links c: inode creation timestamp # u : user id of owner md5: MD5 signature # g : group id of owner tiger: tiger signature* # s : size of file rmd160: RMD160 signature* # b : number of blocks (Linux) sha1: SHA1 signature # S : check for growing size # # *) not implemented, - ignored # # Ex: The following entry will scan all the files in /etc, and report # any changes in mode bits, inode number, reference count, uid, # gid, modification and creation timestamp, and the signatures. # However, it will ignore any changes in the access timestamp. # # /etc +p+i+n+u+g+s+m+md5+tiger+rmd160+sha1-a # # Note! '+' (plus) has presedence over '-' (minus), so the rule +p-p gives +p! # Note2! You MUST have assign +/- (plus/minus) for attributes! If not, the # attribute will be ignored! # # The following templates have been pre-defined to make these long ignore # mask descriptions unecessary. # # Templates: # R : [R]ead-only (+p+i+n+u+g+s+b+m+md5+tiger+rmd160+sha1-a) # L : [L]og file (+p+i+n+u+g-s-b-a-m-md5-tiger-rmd160-sha1) # N : ignore [N]othing (+p+i+n+u+s+b+g+s+a+m+c+md5+tiger+rmd160+sha1) # E : ignore [E]verything (-p-i-n-u-s-b-g-s-a-m-c-md5-tiger-rmd160-sha1) # > or Logs : growing logfile (+p+u+g+i+n+S) # Binlib : binary files (+p+i+n+u+g+s+b+m+c+md5+sha1) # ConfFiles : config files (+p+i+n+u+g+s+b+m+c+md5+sha1) # Devices : device files /dev (+p+i+n+u+g+s+b+c) # Databases : databases (+p+n+u+g) # StaticDir : static directories (+p+i+n+u+g) # ManPages : manual pages (+p+i+n+u+g+s+b+m+c+md5+sha1) # # You can use templates with modifiers, like: # Ex: /etc/lp E+u+g # # Example configuration file: # /etc R # all system files # !/etc/lp R # ...but not those logs # =/tmp N # just the directory, not its files # # Note the difference between pruning (via '!') and ignoring everything # (via 'E' template): Ignoring everything in a directory still monitors # for added and deleted files. Pruning a directory will prevent Pide # from even looking in the specified directory. # # Running slowly? Modify the entries to ignore one of the signatures (md5 # or sha1) when this computationally-exorbitant protection is a paranoia # setting only :^) # # Where is our database file? database=file:/var/db/pide.db # New databases (--init) database_out=file:/var/db/pide.db.new # Change this to 'no', or remove it to not gzip output # (only useful on systems with few CPU cycles to spare) gzip_dbout=yes # Lengt of summary when doing --check or --update # short = one line telling whats changed # normal = same as above and a list of changed files # detail = same as above and a detailed list of change summary=normal # Whether to warn about dead symlinks or not (default). warn_dead_symlinks=no # Custom rules may go here # NB!! '-' (minus) has presedence over '+' (plus), so the rule +p-p gives -p! # You may specify enviroment variables to be exported when running here. # It's normally only usful when used by the cron script. # Define variable VAR to value val # @@define VAR val @@define MAILTO root@localhost @@define LINES 1000 # You may undefine enviroment variables with # @@undef VAR # Next decide what directories/files you want in the database # - You may do globbing here; /bin/file* # - You may also use '$' to define end of file/dir (ex.: '/tmp$'), but # is not necessary, since python's glob does the job fine without - # so it's stripped. It's supported for compatibility with # aide/tripwire only. # Homes =/ L # First, root's traditional home /root R # Most likely root's home !/root/.bash_history =/home L # Holding the users homedir # Log files =/var/log StaticDir /var/log Logs # Kernel, system map etc. - files that are used by the boot loader. /boot Binlib # System configuration files /etc R /usr/local/etc R # Binaries /bin Binlib /sbin Binlib /usr/bin Binlib /usr/sbin Binlib /usr/local/bin Binlib /usr/local/sbin Binlib /usr/games Binlib # Libraries /lib Binlib /usr/lib Binlib /usr/local/lib Binlib # Databases /var/db Databases # Test only the directory when dealing with /proc and /tmp =/proc StaticDir =/tmp StaticDir # You can look through these examples to get further ideas # manpages can be trojaned, especially depending on *roff implementation #/usr/man ManPages #/usr/share/man ManPages #/usr/local/man ManPages # docs #/usr/doc ManPages #/usr/share/doc ManPages # check users' home directories #/home Binlib # Devices - be careful to exclude terminals, sound devices, ++ #/dev Devices # check sources for modifications #/usr/src L #/usr/local/src L # Check headers for same #/usr/include L #/usr/local/include L """ ########################## usage def usage(error): """Print usage of program""" if error: print "%s: %s" % (os.path.basename(sys.argv[0]), str(error)) print "Try `%s --help' for more information." % os.path.basename(sys.argv[0]) else: print """usage: %s [OPTIONS] %s (Python Intrusion Detection Enviroment) is an intrusion detection system for checking the integrity of files. Basically a python implemenation of Tripwire/AIDE. Mandatory arguments to long options are mandatory for short options too. -i, --init Initialize the database. -C, --check Checks the database for inconsistencies. This is the default option. -u, --update Checks the database and updates the database non-interactively. -c, --config=FILE Read config options from FILE -p, --printconf View an example config file. Do a '%s -p > pide.conf' to pipe this to a config file. -v, --verbose Level of debug messages. -h, --help Display this help and exit. Report bugs to lars [at] gnist org""" % (os.path.basename(sys.argv[0]), os.path.basename(sys.argv[0]), os.path.basename(sys.argv[0])) ########################## main if __name__ == '__main__': """Main loop""" # version control version = string.split(string.split(sys.version)[0], ".") if map(int, version) < [2, 3]: usage("You need Python ver 2.3 or higher to run!") sys.exit(1) try: # opts = arguments recognized, # args = arguments NOT recognized (leftovers) opts, args = getopt.getopt(sys.argv[1:], "iCuhvc:p", ["init", "check", "update", "help", "verbose", "config=", "printconf"]) except getopt.GetoptError: # print help information and exit: usage("illegal option(s) -- " + str(sys.argv[1:])) sys.exit(1) # try this as default pide.conf file, if none given rcfile = '/etc/pide.conf' # default verbose level verbose = 0 # run through arguments and set variables # FIXFIX! - What if config file is specified AFTER the --init, # must get config file BEFORE init, check and update! for o, a in opts: if o == "-c" or o == "--config": # read config from this file try: # check for readability later rcfile = str(a) except: usage("invalid config file") sys.exit(1) if o == "-v" or o == "--verbose": # verbose level verbose = 1 #print "config file:", rcfile for o, a in opts: if o == "-h" or o == "--help": # display help and exit usage("") sys.exit(0) if o == "-p" or o == "--printconf": # display example config file printconf() sys.exit(0) if o == "-i" or o == "--init": # initialize the database integrityCheck(init=1, rcfile=rcfile, verbose=verbose) sys.exit(0) if o == "-C" or o == "--check": # check for inconsistecies integrityCheck(check=1, rcfile=rcfile, verbose=verbose) sys.exit(0) if o == "-u" or o == "--update": # update the database integrityCheck(update=1, rcfile=rcfile, verbose=verbose) sys.exit(0) # if there is no options, do default (--check) if not opts: integrityCheck(check=1) sys.exit(0) # end |