Check-in [5f35cb034d]
Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Initial prototype (conf parser, log reader, mainwindow somewhat functional) |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk | 0.0.9 |
Files: | files | file ages | folders |
SHA3-256: |
5f35cb034d0f79cc29b25b510c4924a8 |
User & Date: | mario 2020-11-13 14:50:18 |
Context
2020-11-13
| ||
15:20 | Update README with GIF, use .rst for pkg README. check-in: fd5f570868 user: mario tags: trunk | |
14:50 | Initial prototype (conf parser, log reader, mainwindow somewhat functional) check-in: 5f35cb034d user: mario tags: trunk, 0.0.9 | |
14:46 | initial empty check-in check-in: 4989e432a6 user: mario tags: trunk | |
Changes
Added Makefile.
> > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 | run: ./g.py setup: version --read modseccfg/__init__.py --incr --write python3 setup.py bdist_wheel rm -r modseccfg.egg-info upload: python3 setup.py upload |
Added README.md.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | > *WARNING: THIS IS ALPHA STAGE QUALITY AND WILL MOST CERTAINLY DELETE YOUR APACHE CONFIGURATION* > (It doesn't, but: no waranty and such.) ## modseccfg * Simple GUI editor for SecRuleDisableById settings * Tries to suggest false positives from error and audit logs * And a few options to configure mod_security and CRS variables. * Obviously requires `ssh -X` forwarding, or preparing config rules on a local test setup, and `*.conf` files to be writable by current user (running as root is not advised). # Usage You obviously should have Apache(2.x) + mod_security(2.9) + CRS(3.x) set up and running already (in DetectionOnly mode initially), to allow for log inspection and adapting rules. 1. start modseccfg (`python3 -m modseccfg`) 2. Select a configuration/vhost file to inspect + work on. 3. Pick the according error.log 4. Inspect the rules with a high error count. 5. [Disable] offending rules (if they're not essential to CRS, or would likely poke holes into useful protections). 6. Thenceforth restart Apache after testing changes (`apache2ctl -t`). ## Notes * Preferrably do not edit default `/etc/apache*` files * Work on separated `/srv/web/conf.d/*` configuration, if available * And keep vhost settings in e.g. `vhost.*.dir` files, rather than multiple `<VirtualHost>` in one `*.conf` (else only the first section will be augmented). ## Missing features * Doesn't process any audit.log yet. * Can't classify wrapped (`<Location>` or other directives) rules yet. * No rule information dialog. * No SecOption editor yet. * No CRS settings (setvar:crsโฆ) editor yet. * Recipes are not worth using yet. * No sudo usage. * No support for nginx or mod_sec v3. |
Added g.py.
> > > | 1 2 3 | #!/usr/bin/env python3 import modseccfg.mainwindow modseccfg.mainwindow.main() |
Added modseccfg/__init__.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | # encoding: utf-8 # api: python # type: init # title: modseccfg # description: Editor to tame mod_security rulesets # version: 0.0.9 # state: prototype # support: none # license: ASL # depends: python:pysimplegui (>= 3.0), python:pluginconf (>= 0.7.2) # priority: core # url: https://fossil.include-once.org/modseccfg/ # category: config # classifiers: x11, http # # Correlates mod_security SecRules to logs, and simplifies # disabling unneeded rules. It's very basic and not gonna # win any usability awards. # BE WARNED THAT ALPHA RELEASES MAY DAMAGE YOUR APACHE SETUP. # # Basically you select your desired vhost *.conf file, then # hit [Disable] for rules with a high error.log count - if it's # false positives. Preferrably leave rules untouched that are # indeed working as intended. # |
Added modseccfg/__main__.py.
> > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # api: python # type: virtual # priority: hidden # title: module invocation # description: python -m modseccfg # version: 0.0 # category: cli # # Just a wrapper to allow starting module directly. # if __name__ == "__main__": from modseccfg.mainwindow import main main() |
Added modseccfg/appsettings.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | # api: python # type: io # title: app settings # description: read/store modseccfg options # depends: pluginconf (>= 0.7.2), python:appdirs (>= 1.3), python:pathlib # category: config # version: 0.1 # config: # { name: "test[key]", type: bool, value: 1, description: "Array[key] test" } # # Basically just a dictionary for the GUI and some # module behaviours. GUI courtesy of pluginconf. # import json, os from modseccfg.utils import expandpath import pluginconf, pluginconf.gui #import appdirs # defaults conf = { "theme": "DarkGrey", "edit_sys_files": False, "backup_files": True, "log_entries": 5000, "log_filter": "(?!404|429)[45]\d\d", "log_skip_rx" : "PetalBot|/.well-known/ignore.cgi", "max_rule_range": 1000, # obsolete already (SecRuleDisById ranges do a lookup) "backup_dir": expandpath("~/backup-config/"), "conf_file": expandpath("~/.config/modseccfg.json") } # plugin lookup pluginconf.module_base=__name__ pluginconf.plugin_base=["modseccfg"] for module,meta in pluginconf.all_plugin_meta().items(): pluginconf.add_plugin_defaults(conf, {}, meta, module) #print(__package__) #print(pluginconf.module_list()) #print(conf) # read config file def read(): if os.path.exists(conf["conf_file"]): conf.update(json.load(open(conf["conf_file"], "r"))) # write config file def write(): if not os.path.exists(os.path.dirname(conf["conf_file"])): os.mkdir(os.path.dirname(conf["conf_file"])) print(str(conf)) json.dump(conf, open(conf["conf_file"], "w"), indent=4) # show config option dialog def window(mainself, *kargs): pluginstates = {"mainwindow": 1, "__init__":1,"appsettings":1,"vhosts":1,"logs":1,"writer":1} fn_py = __file__.replace("appsettings", "*") save = pluginconf.gui.window(conf, pluginstates, files=[fn_py], theme=conf["theme"]) if save: write() # initialze conf{} read() |
Added modseccfg/icons.py.
> > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 | # api: pysimplegui # type: data # category: image # title: icons # description: embedded PNG files for the GUI # version: 0.0 # # Icons for the UI. # logo = b"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" vice = b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAC7AAAAuwB7TO0+gAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAALdSURBVDiNdZNNaFxVGIafc+bMnUkmM5lMm050EpVEbVJEqalICiqajT8LV13EVepCRdyoiAgG6l5UqlKIG3UnRcG6E9RgCRZtaKINdJo01doQEgyTtM69d+6ccz4XGcIk4Lv5zvfD+/3wHsUOBoAckAY22IsOoNR6/w7Y9qRu2SwQAHnAtGL3tdVlgUyrwf8iAFSb306g24j3QAVB8GB/wQTFLmPq/waL1YmJZGhhcdRis6bRELW9KU1pKothMwlc7Jwnlw05dvwmE2e21ZFy9/j81Hjtk5lrB7/+Y/XybFTsMa9/3MuRh9MUSqEWfwtB9ndOXhw7JG9ULpjrm/X42U9nS08/0LtTZAnUPYdDKZYycu6Lkl9eyAGQ6/Z6cmqDAPCB4H3qvbVzsYmsvdSI5LGUUk5rBFyo83kRZ3Qy820XWyszLC4mVAbHg1emHE0UJNBMOk990D1oAGuNJVBaayf9D1UKYf7V0cho7a331Z68Nd9BgvOWSz9rtFLitAreObOSfHbqfgNYLaKfGumNOoJMXqXINtN3ayeiltZqhcs3agEiVbq6qslbL+R3j9C45WmoKwbwCiXDlQPxcN+BCMFhrAb48nxT/VZd0yglQBPq0f5jGjn9TObx03Myu7xuPvp+6aA2uvnVyUcSAGNgZSMqAOWh3tzRd58bXgVwKSUk8NrZ+R5DZsCnzZwsXL+dvrIeXb1Wu30HWnmAo3cVt08cq+Asdw715cPJ4/dm8VYQ3PPTvwxorVaNenk6++TwIaVS2u+qTkwIMFIu8uGJ0dasvo7DorTHk1xYrv0Ths05M1DKjZ0cG9wKvcuEiALR+/fc0azK4pz8tR3L29/MFw+Xuy+uv/moMfWGVWd//TsX+UZHHMcCyifWq8DoveoTid//8ar5YWm9q6/U8dP5izdjnhg2SgTF55MZkkDz0nTU2Un/SLlQiRuy+/MyqZSOnZW1mr2xFcd/tvP+B4DSPiSzfp3PAAAAAElFTkSuQmCC" |
Added modseccfg/logs.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | # api: modseccfg # encoding: utf-8 # type: file # category: log # title: log reader # description: scan error.logs / audit log # config: # { name: log_entries, type: int, value: 5000, description: "How many log entries to show (whole log will be counted regardless)" } # { name: log_filter, type: str, value: "(?!404|429)4\d\d|5\d\d", description: "Error codes to look out for in access.logs" } # { name: log_skip_rx, type: str, value: "PetalBot|/.well-known/ignore.cgi", description: "Regex to skip whole log lines on" } # { name: log_search_id, type: bool, value: 0, description: "Look up rule id, if only file+line given in log (slow)" } # version: 0.1 # # Basic filtering and searching within the logs. # Filters out by error codes (http 4xx/5xx) or mod_security messages. # # Audit log types (serial/concurrent/json) aren't supported yet. # import os, re from modseccfg import utils, appsettings, vhosts # detected rule ids and number of occurences log_count = {} # idโcount class state: log_curr = "" # fn # extraction rules class rx: interesting = re.compile(""" ModSecurity: | \[id\s"\d+"\] | "\s((?!429)[45]\d\d)\s\d+ # should come from conf[log_filter] """, re.X) id = re.compile(""" (?:\[id\s|\{"id":\s*)"(\d+)"[\]\}] # [id "โฆ"] or json {"id":"โฆ"} """, re.X) file_line = re.compile(""" \[file \s "(?P<file>.+?)"\] \s* \[line \s "(?P<line>\d+)"\] """, re.X) shorten = re.compile(""" :\d\d.\d+(?=\]) | \s\[pid\s\d[^\]]*\] | \s\[tag\s"[\w\-\.\/]+"\] | \s\[client\s[\d\.:]+\] | \sRule\s[0-9a-f]{12} | (?<=\[file\s")/usr/share/modsecurity-crs/rules/ | """, re.X) # search through log file, filter, extract rule ids, return list of log lines def scan_log(fn): if fn == state.log_curr: return # no update state.log_curr = "" if not os.path.exists(fn): return log_curr = fn # filter lines log_lines = [] for line in open(fn, "r"): if rx.interesting.search(line): if re.search(appsettings.conf["log_skip_rx"], line): continue m = rx.id.search(line) if m: incr_log_count(int(m.group(1))) elif appsettings.conf["log_search_id"]: m = rx.file_line.search(line) if m: id = search_id(m.group("file"), m.group("line")) if id: incr_log_count(int(id)) log_lines.append(line.strip()) # slice entries if len(log_lines) >= appsettings.conf["log_entries"]: log_lines = log_lines[-appsettings.conf["log_entries"]:] # shorten infos in line log_lines = [rx.shorten.sub("", line) for line in log_lines] return log_lines # count++ def incr_log_count(id): if id in log_count: log_count[id] += 1 else: log_count[id] = 1 # search [id โฆ] from only [file โฆ] and [line โฆ] - using vhosts.linemap{} def search_id(file, line): print("linemap:", file, line) if file and line: vh = vhosts.vhosts.get(file) if vh: return vh.line_to_id(int(line)) return 0 # assemble list of error/access/audit logs def find_logs(): log_list = [] for fn,vh in vhosts.vhosts.items(): log_list = log_list + vh.logs log_list.append("./fossil.error.log") # testing return list(set(log_list)) |
Added modseccfg/mainwindow.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | # encoding: utf-8 # api: python # type: main # title: main window # description: GUI with menus, actions, rules and logs # category: config # version: 0.1.0 # state: alpha # license: ASL # config: # { name: theme, type: select, value: DarkRed1, select: "Default|DarkGrey|Black|BlueMono|BluePurple|BrightColors|BrownBlue|Dark|Dark2|DarkAmber|DarkBlack|DarkBlack1|DarkBlue|DarkBlue1|DarkBlue10|DarkBlue11|DarkBlue12|DarkBlue13|DarkBlue14|DarkBlue15|DarkBlue16|DarkBlue17|DarkBlue2|DarkBlue3|DarkBlue4|DarkBlue5|DarkBlue6|DarkBlue7|DarkBlue8|DarkBlue9|DarkBrown|DarkBrown1|DarkBrown2|DarkBrown3|DarkBrown4|DarkBrown5|DarkBrown6|DarkBrown7|DarkGreen|DarkGreen1|DarkGreen2|DarkGreen3|DarkGreen4|DarkGreen5|DarkGreen6|DarkGreen7|DarkGrey|DarkGrey1|DarkGrey10|DarkGrey11|DarkGrey12|DarkGrey13|DarkGrey14|DarkGrey2|DarkGrey3|DarkGrey4|DarkGrey5|DarkGrey6|DarkGrey7|DarkGrey8|DarkGrey9|DarkPurple|DarkPurple1|DarkPurple2|DarkPurple3|DarkPurple4|DarkPurple5|DarkPurple6|DarkPurple7|DarkRed|DarkRed1|DarkRed2|DarkTanBlue|DarkTeal|DarkTeal1|DarkTeal10|DarkTeal11|DarkTeal12|DarkTeal2|DarkTeal3|DarkTeal4|DarkTeal5|DarkTeal6|DarkTeal7|DarkTeal8|DarkTeal9|Default|Default1|DefaultNoMoreNagging|Green|GreenMono|GreenTan|HotDogStand|Kayak|LightBlue|LightBlue1|LightBlue2|LightBlue3|LightBlue4|LightBlue5|LightBlue6|LightBlue7|LightBrown|LightBrown1|LightBrown10|LightBrown11|LightBrown12|LightBrown13|LightBrown2|LightBrown3|LightBrown4|LightBrown5|LightBrown6|LightBrown7|LightBrown8|LightBrown9|LightGray1|LightGreen|LightGreen1|LightGreen10|LightGreen2|LightGreen3|LightGreen4|LightGreen5|LightGreen6|LightGreen7|LightGreen8|LightGreen9|LightGrey|LightGrey1|LightGrey2|LightGrey3|LightGrey4|LightGrey5|LightGrey6|LightPurple|LightTeal|LightYellow|Material1|Material2|NeutralBlue|Purple|Python|Reddit|Reds|SandyBeach|SystemDefault|SystemDefault1|SystemDefaultForReal|Tan|TanBlue|TealMono|Topanga", description: "PySimpleGUI window theme", help: "Requires a restart to take effect." } # priority: core # classifiers: x11, http # # The main window binds all processing logic together. Lists # primarily the SecRules and their states (depending on the # selected vhost/*.conf file). Then allows to search through # logs to find potential false positives. # import sys, os, re, json, subprocess from modseccfg import utils, appsettings, icons, vhosts, logs, writer from modseccfg.recipe import recipe import PySimpleGUI as sg sg.theme(appsettings.conf["theme"]) #-- init rule_tree = sg.TreeData() vhosts.scan_all() #-- prepare vhost/rules/logs for UI structures class ui_data: @staticmethod def rules(log_count={}, rulestate={}): rule_tree = sg.TreeData() hidden = [0] for id,r in vhosts.rules.items(): # skip control rules if r.pattern == "@eq 0" or r.vars == "TX:EXECUTING_PARANOIA_LEVEL": hidden.append(id) continue parent = "" if r.chained_to: parent = r.chained_to if parent in hidden: continue # prepare treedata attributes state = rulestate.get(id) if state in (0, "off"): state = "โ" elif state in (-1, "change"): state = "โ" else: state = "โ " msg = r.msg or r.params.get("logdata") or "{} {}".format(r.vars, r.pattern) rule_tree.insert( parent=parent, key=id, text=id, values=[ state, str(id), msg, r.tag_primary, log_count.get(id, 0) ], icon=icons.vice #ui_data.img_vice ) return rule_tree #-- widget structure layout = [ [sg.Column([ # menu [sg.Menu([ ['File', ['Rescan configs', 'Rescan logs', 'Test', 'Settings', 'Exit']], ['Rule', ['Disable', 'Modify', 'Enable']], ['Recipe', ['<Location>', '<FilesMatch>', '<Directory>', "Exclude parameter", "ConvertToRewriteRule"]], ['Help', ['Advise']] ], key="menu" )], # button row [ sg.Button("โ Disable"), sg.Button("โ Modify",disabled=1), sg.Button("โ Enable"), sg.Button("โฎโฏ Wrap",disabled=1) ], # comboboxes [sg.Text("vhost/conf", font="bold"), sg.Combo(key="confn", size=(50,1), values=vhosts.list_vhosts(), enable_events=True), sg.Text("Log"), sg.Combo(key="logfn", values=logs.find_logs(), size=(30,1), enable_events=True), ], ]), # logo sg.Column([ [sg.Image(data=icons.logo)] ], element_justification='r', expand_x=1), ], # tabs [sg.TabGroup([[ # rule sg.Tab(" SecRules ", [[ sg.Tree( key="rule", data=ui_data.rules(), headings=["โ","RuleID","Description","Tag","Count"], col0_width=0, col_widths=[1,10,65,15,10], max_col_width=500, justification="left", show_expanded=0, num_rows=30, auto_size_columns=False ) #], expand_x=1, expand_y=1, size=(600,500)) ]]), # log sg.Tab(" Log ", [[ sg.Listbox(values=["... 403", "... 500"], size=(980,650), key="log") ]]) ]], key="active_tab")], [sg.Text("...", key="status")] ] #-- GUI event loop and handlers class gui_event_handler: # prepare window def __init__(self): self.w = sg.Window(title="mod_security config", layout=layout, size=(1200,775), font="Sans 12", resizable=1) self.tab = "secrules" self.status = self.w["status"].update self.vh = None # event loop and function/module dispatching def main(self): while True: event, data = self.w.read() if event == sg.WIN_CLOSED: event, data = "exit", {} event = self._case(data.get("menu") or event) self.tab = self._case(data.get("active_tab", "")) if event and hasattr(self, event): getattr(self, event)(data) elif recipe.has(event): recipe.show(event, data) elif event == "exit": break else: #self.status(value= print("UNKNOWN EVENT: {} / {}".format(event, str(data))) self.w.close() # change in vhost combobox def confn(self, data): self.vh = vhosts.vhosts.get( data.get("confn") ) logfn = data.get("logfn") # switch logfn + automatically scan new error.log? self.update_rules() # scan/update log def logfn(self, data): self.w["log"].update( logs.scan_log(data["logfn"]) ) self.update_rules() # SecRuleDisableById def disable(self, data): id = data["rule"] if id: id = id[0] else: return fn = data["confn"] if fn and id: writer.append(fn, "SecRuleDisableById", id) self.vh.ruledecl[id] = 0 self.update_rules() # remap 'settings' event to pluginconf window def settings(self, data): appsettings.window(self) # renew display of ruletree with current log and vhost rulestate def update_rules(self): if self.vh: self.w["rule"].update(ui_data.rules(log_count=logs.log_count, rulestate=self.vh.rulestate)) # remove non-alphanumeric characters (for event buttons / tab titles / etc.) def _case(self, s): return re.sub("\W+", "_", str(s)).strip("_").lower() # tmp/dev def test(self, data): print("No test code") #-- main def main(): gui_event_handler().main() |
Added modseccfg/recipe.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | # api: modseccfg # encoding: utf-8 # version: 0.0 # type: data # title: recipe # description: Apache/mod_security config examples or conversions # category: config # config: # { name: replace_rules, type: bool, value: 0, description: "try to find replacement spot, else just append" } # # Basically just blobs of text and an editor window. # [Save] will append directives to selected vhost/*.conf file. # from modseccfg import appsettings, vhosts import PySimpleGUI as sg import re from textwrap import dedent class recipe: location = """ <Location "/app/"> SecRuleDisableById $id #@wrap </Location> """ exclude_parameter = """ SecRuleUpdateTargetByID $id "!ARGS:param" """ macros = """ <IfModule mod_alias.c> <Macro SecRuleDisableByPath $id $path> SecRule REQUEST_URI "@eq $path" "id:%{md5:$id-$path},t:none,msg:'Whitelist $path',ctl:removeById=$id" </Macro> </IfModule> """ @staticmethod def has(name): return hasattr(recipe, name) @staticmethod def show(name, data, id=0, vhost={}): # resolve text = getattr(recipe, name) if type(text) is str: text = dedent(text) if re.search(r"\$(id|path|tag)", text): if data.get("rule"): text = re.sub(r"\$id", str(data["rule"][0]), text) #@ToDo: mainwindow should supply a data bag (either full secrule entry, or params from log - depending on which is active) else: text = text(data) print(data) print(text) # window w = sg.Window(title="Recipe '{}'".format(name), layout=[ [sg.Multiline(default_text=text, key="src", size=(80,20), font="Sans 14")], [sg.Button("Save", key="save"), sg.Button("Cancel", key="cancel")] ]) event, values = w.read() print(event, values) w.close() # write โฆ pass |
Added modseccfg/utils.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | # encoding: utf-8 # api: python # # Monkeypatching some Python modules, and some convenience wrappers #-- path from pathlib import Path as path def expandpath(dir): return str(path(dir).expanduser()) #-- patch re import re def re_compile(regex, *kargs, **kwargs): if type(regex) is str: regex = re.sub('\\\\h', '[\\ \\t\\f]', regex) return re.compile_orig(regex, *kargs, **kwargs) re.compile_orig = re.compile re.compile = re_compile def re_grep(regex, list): return (s for s in list if re.search(regex, s)) re.grep = re_grep #-- import frosch for prettier exceptions try: import frosch frosch.hook() except: pass |
Added modseccfg/vhosts.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | # api: modseccfg # encoding: utf-8 # title: *.conf scanner # description: Compiles a list of relevant apache/vhost files and Sec* settings # type: tokenizer # category: apache # version: 0.5 # config: # { name: envvars, value: "/etc/default/apache", type: str, description: "Look up APACHE_ENV vars from shell script", help: "Mostly applies to Debian derivates. Other distros usually embed SetEnv directives for log paths." } # license: ASL # # Runs once to scan for an vhost* and mod_security config files. # Uses `apache2ctl -t -D DUMP_INCLUDES` to find all includes, # and regexes for Sec*Rules or *Log locations and ServerNames. # # This should yield any mod_security and vhost-relevant sections. # The list is kept in `vhosts`. And secrule declarations+options # in `rules`. # # Extraction is fairly simplistic, but for this purpose we don't # need an exact representation nor nested structures. The UI will # present vhost/conf files rather than <VirtualHost> sections. # (We shouldn't penalize the average user for random edge cases.) # # Notably this will not work with mod_security v3, since the # SecRules/Flags have been moved into matryoshka directives and # external *.conf files (no JSON rulesets for some reason). # Still doable, but not a priority right now. Same for nginx. # import os, re import subprocess import traceback from pprint import pprint # collected config/vhost sections vhosts = { # fn โ vhost: # .fn .t .name .logs[] .cfg{} .rulestate{} .ruledecl{} .linemap{} } # and SecRules (we don't actually use all extracted details) rules = { # id โ secrule: # .id .chained_to .msg .flags{} .params{} .tags[] .tag_primary .ctl{} .setvar{} .vars .pattern } # extraction patterns class rx: dump_includes = re.compile("^\s*\([\d*]+\)\s+(.+)$", re.M) # directives we care about interesting = re.compile( "^ \s* (ErrorLog | CustomLog | Server(Name|Alias) | (Virtual)?DocumentRoot | Sec\w* | Use\sSec\w+ | modsecurity\w* ) \\b", re.M|re.I|re.X ) # extract directive line including line continuations (<\><NL>) configline = re.compile( """ ^ [\ \\t]* # whitespace \h* # (?:Use \s{1,4})? # optional: `Useโฃ` to find custom macros like `Use SecRuleDisableByPathโฆ` (\w+) # alphanumeric directive [\ \\t]+ # whitespace \h+ ( (?: [^\\n\\\\]+ | [\\\\]. )* # literals, or backslash + anything ) $ """, re.M|re.S|re.X ) # to strip <\><NL> escnewline = re.compile( """[\\\\][\\n]\s*""" # escaped linkebreaks ) # handle quoted/unquoted directive arguments (not entirely sure if Apache does \" escaped quotes within) split_args = re.compile( """ (?:\s+) | # skip whitespace (\K not supported in python re, so removing empty matches in postprocessing) \#.*$ | # skip trailing comment (which isn't technically allowed, but) " ((?:[^\\\\"]+|\\\\ .)+) " | # quoted arguments (?!\#) ([^"\s]+) # plain arguments (no quotes, no spaces) """, re.X ) # SecRule โฆ โฆ `actions:argument,โฆ` actions = re.compile( """ (?: (t|pfx) :)? (\w+) # action (?: : # : valueโฆ ([^,']+) | # bareword : ' ([^']+) ' # ' quoted ' )? """, re.X ) # line number scan: roughly look for id:123456 occurences id_num = re.compile( "id:(\d+)" # without context ) # comment lookup, directly preceeding id, uncompiled rule_comment = """ ( (?:^\#.+$ | ^\s*\n)+ ) # consecutive comment lines [\s\S]+? # minimal amount of anything id:{} # id:nnnnn, requires "โฆ{}โฆ".format($id) """ # temporary state variables class tmp: last_rule_id = 0 tag_prio = ['event-correlation', 'anomaly-evaluation', 'OWASP_CRS/LEAKAGE/ERRORS_IIS', 'OWASP_CRS/LEAKAGE/SOURCE_CODE_PHP', 'OWASP_CRS/LEAKAGE/ERRORS_PHP', 'OWASP_CRS/LEAKAGE/ERRORS_JAVA', 'OWASP_CRS/LEAKAGE/SOURCE_CODE_JAVA', 'platform-sybase', 'platform-sqlite', 'platform-pgsql', 'platform-mysql', 'platform-mssql', 'platform-maxdb', 'platform-interbase', 'platform-ingres', 'platform-informix', 'platform-hsqldb', 'platform-frontbase', 'platform-firebird', 'platform-emc', 'platform-db2', 'platform-oracle', 'CWE-209', 'OWASP_CRS/LEAKAGE/ERRORS_SQL', 'platform-msaccess', 'OWASP_CRS/LEAKAGE/SOURCE_CODE_CGI', 'PCI/6.5.6', 'WASCTC/WASC-13', 'OWASP_CRS/LEAKAGE/INFO_DIRECTORY_LISTING', 'attack-disclosure', 'OWASP_CRS/WEB_ATTACK/JAVA_INJECTION', 'language-java', 'CAPEC-61', 'WASCTC/WASC-37', 'OWASP_CRS/WEB_ATTACK/SESSION_FIXATION', 'attack-fixation', 'OWASP_AppSensor/CIE1', 'WASCTC/WASC-19', 'OWASP_CRS/WEB_ATTACK/SQL_INJECTION', 'attack-sqli', 'PCI/6.5.1', 'OWASP_TOP_10/A2', 'CAPEC-63', 'platform-internet-explorer', 'platform-tomcat', 'CAPEC-242', 'OWASP_AppSensor/IE1', 'OWASP_TOP_10/A3', 'WASCTC/WASC-22', 'WASCTC/WASC-8', 'OWASP_CRS/WEB_ATTACK/XSS', 'attack-xss', 'OWASP_CRS/WEB_ATTACK/NODEJS_INJECTION', 'attack-injection-nodejs', 'language-javascript', 'OWASP_CRS/WEB_ATTACK/PHP_INJECTION', 'attack-injection-php', 'language-php', 'language-powershell', 'PCI/6.5.2', 'WASCTC/WASC-31', 'OWASP_CRS/WEB_ATTACK/COMMAND_INJECTION', 'attack-rce', 'platform-unix', 'language-shell', 'OWASP_CRS/WEB_ATTACK/RFI', 'attack-rfi', 'PCI/6.5.4', 'OWASP_TOP_10/A4', 'WASCTC/WASC-33', 'OWASP_CRS/WEB_ATTACK/FILE_INJECTION', 'OWASP_CRS/WEB_ATTACK/DIR_TRAVERSAL', 'attack-lfi', 'OWASP_CRS/WEB_ATTACK/HTTP_PARAMETER_POLLUTION', 'CAPEC-460', 'OWASP_CRS/WEB_ATTACK/HEADER_INJECTION', 'OWASP_CRS/WEB_ATTACK/RESPONSE_SPLITTING', 'OWASP_CRS/WEB_ATTACK/REQUEST_SMUGGLING', 'paranoia-level/4', 'language-aspnet', 'paranoia-level/3', 'OWASP_CRS/PROTOCOL_VIOLATION/MISSING_HEADER_UA', 'OWASP_CRS/POLICY/HEADER_RESTRICTED', 'OWASP_CRS/POLICY/EXT_RESTRICTED', 'OWASP_CRS/POLICY/PROTOCOL_NOT_ALLOWED', 'OWASP_CRS/PROTOCOL_VIOLATION/CONTENT_TYPE_CHARSET', 'OWASP_CRS/POLICY/CONTENT_TYPE_NOT_ALLOWED', 'OWASP_AppSensor/EE2', 'OWASP_TOP_10/A1', 'WASCTC/WASC-20', 'OWASP_CRS/PROTOCOL_VIOLATION/CONTENT_TYPE', 'OWASP_CRS/POLICY/SIZE_LIMIT', 'OWASP_CRS/PROTOCOL_VIOLATION/IP_HOST', 'OWASP_CRS/PROTOCOL_VIOLATION/EMPTY_HEADER_UA', 'OWASP_CRS/PROTOCOL_VIOLATION/MISSING_HEADER_ACCEPT', 'OWASP_CRS/PROTOCOL_VIOLATION/MISSING_HEADER_HOST', 'platform-windows', 'platform-iis', 'OWASP_CRS/PROTOCOL_VIOLATION/EVASION', 'OWASP_CRS/PROTOCOL_VIOLATION/INVALID_HREQ', 'CAPEC-272', 'OWASP_CRS/PROTOCOL_VIOLATION/INVALID_REQ', 'attack-protocol', 'OWASP_CRS/AUTOMATION/CRAWLER', 'attack-reputation-crawler', 'OWASP_CRS/AUTOMATION/SCRIPTING', 'attack-reputation-scripting', 'PCI/6.5.10', 'OWASP_TOP_10/A7', 'WASCTC/WASC-21', 'OWASP_CRS/AUTOMATION/SECURITY_SCANNER', 'attack-reputation-scanner', 'paranoia-level/2', 'attack-dos', 'PCI/12.1', 'OWASP_AppSensor/RE1', 'OWASP_TOP_10/A6', 'WASCTC/WASC-15', 'OWASP_CRS/POLICY/METHOD_NOT_ALLOWED', 'OWASP_CRS', 'IP_REPUTATION/MALICIOUS_CLIENT', 'attack-reputation-ip', 'platform-multi', 'attack-generic', 'platform-apache', 'language-multi', 'application-multi', 'paranoia-level/1'] env = { "APACHE_LOG_DIR": "/var/log/apache2" #/var/log/httpd/ } env_locations = [ "/etc/apache2/envvars", "/etc/default/httpd" ] # encapsulate properties of config file (either vhosts, SecCfg*, or secrule collections) class vhost: # split *.conf directives, dispatch onto handlers def __init__(self, fn, src): # vhost properties self.fn = fn # retain filename self.t = "cfg" # *.conf type (rules|vhost|cfg) self.name = "" # ServerName self.logs = [] # error.log/access.log self.cfg = {} # SecRuleEngine/settings self.rulestate = {} # SecRuleDisableById self.ruledecl = {} # SecRule IDs โ rules{} self.linemap = {} # LineNo โ RuleID (for looking up chained rules in error.log) self.mk_linemap(src) # id โ source line self.xincludes = [] # modsec v3 includes # extract directive lines for dir,args in rx.configline.findall(src): # or .finditer()? to record positions right away? dir = dir.lower() #print(dir, args) if hasattr(self, dir): # src.find(โฆ) + str position to linecounter... / or do a separate line indexer scan (tx.msg:โฆ -> id:โฆ) func = getattr(self, dir) func(self.split_args(args)) elif dir.startswith("sec"): self.cfg[dir] = args # determine config file type if self.name: self.t = "vhost" elif len(self.rulestate) >= 5: self.t = "cfg" elif len(self.ruledecl) >= 5: self.t = "rules" # strip \\ \n line continuations, split all "args" def split_args(self, args): args = re.sub(rx.escnewline, " ", args) args = rx.split_args.findall(args) args = [s[1] or s[0] for s in args] args = [s for s in args if len(s)] #args = [s.decode("unicode_escape") for s in args] # don't strip backslashes return args # apply ${ENV} vars def var_sub(self, s): return re.sub('\$\{(\w+)\}', lambda m: tmp.env.get(m.group(1), ""), s) # apache: log directives def errorlog(self, args): self.logs.append(self.var_sub(args[0])) def customlog(self, args): self.logs.append(self.var_sub(args[0])) def servername(self, args): self.name = args[0] # modsec: create a rule{} def secrule(self, args): last_id = int(tmp.last_rule_id) r = secrule(args) if r.id: tmp.last_rule_id = r.id elif rules.get(last_id) and "chain" in rules[last_id].flags: tmp.last_rule_id = round(tmp.last_rule_id + 0.1, 1) r.id = tmp.last_rule_id r.chained_to = int(last_id) # primary parent rules[r.id] = self.ruledecl[r.id] = r #print(r.__dict__) # modsec: just a secrule without conditions def secaction(self, args): self.secrule(["@SecAction", "setvar:", args[0]]) # modsec: SecRuleDisableById 900001 900002 900003 def secruledisablebyid(self, args): for a in args: if re.match("^\d+-\d+$", a): # are ranges still allowed? a = [int(x) for x in a.split("-")] for i in range(*a): if i in rules: # only apply state for known/existing rules, not the whole range() self.rulestate[i] = 0 elif re.match("^\d+$", a): self.rulestate[int(a)] = 0 else: self.rulestate[a] = 0 # from tag # modsec: SecRuleDisableByTag sqli app-name - maps onto .secruledisablebyid def secruledisablebytag(self, args): self.secruledisablebyid(args) # these need to be mapped onto existing rules (if within t==cfg) # ยท SecRuleUpdateTargetById # ยท SecRuleUpdateActionById # modssec: irrelevant (not caring about skipAfter rules) def secmarker(self, args): pass # v3-connector: Include def modsecurity_rules_file(self, args): raise Exception("modsecurity v3 connector rules not supported (module doesn't provide disclosure of custom includes via `apache2ctl -t -D DUMP_INCLUDES` yet)") #vhosts[fn] = vhost(args[0], open(args[0], "r", encoding="ascii").read()) # apache: define ENV var def define(self, args): tmp.env[args[0]] = args[1] # map rule ids to line numbers def mk_linemap(self, src): for i,line in enumerate(src.split("\n")): id = rx.id_num.search(line) if id: self.linemap[i] = int(id.group(1)) # find closest match def line_to_id(self, lineno): if not lineno in self.linemap: lines = [i for i in sorted(self.linemap.keys()) if i <= lineno] if lines and lines[-1] in self.linemap: self.linemap[lineno] = self.linemap.get(lines[-1]) return self.linemap.get(lineno, 0) # break up SecRule definition into parameters, attributes (id,msg,tags,meta,actions etc.) class secrule: def __init__(self, args): # secrule properties self.id = 0 self.chained_to = 0 self.msg = "" self.flags = [] self.params = {} self.tags = [] self.tag_primary = "" self.ctl = {} self.setvar = {} self.vars = "REQ*" self.pattern = "@rx ..." # args must contain 3 bits for a relevant SecRule if len(args) != 3: print("UNEXPECTED NUMBER OF ARGS:", args) return self.vars, self.pattern, actions = args #print(args) # split up actions,attributes:โฆ for pfx, action, value, qvalue in rx.actions.findall(actions): #print(pfx,action,value,qvalue) self.assign(pfx, action, value or qvalue) # most specific tag for p in tmp.tag_prio: if p in self.tags: self.tag_primary = p break # if SecAction (uncoditional rule, mostly setvars:) if self.vars == "@SecAction" and not self.msg: self.msg = "@SecAction {}".format(str(self.setvar) if self.setvar else str(self.params)) # distribute actions/attributes into properties here def assign(self, pfx, action, value): if action == "id": self.id = int(value) elif action == "msg": self.msg = value elif action == "tag": self.tags.append(value) elif action == "ctl": if value.find("=") > 0: action, value = value.split("=", 1) self.ctl[action] = value or 1 elif action == "setvar": if value.find("=") > 0: action, value = value.split("=", 1) self.setvar[action] = value elif pfx == "t" and not value: self.flags.append(pfx+":"+action) elif action and not pfx: if value: self.params[action] = value else: self.flags.append(action) else: print(" WHATNOW? ", [pfx, action, value]) # look up doc comment in source file def help(self): id = int(self.id) for fn,vh in vhosts.items(): if id in vh.ruledecl: src = open(fn, "r").read().decode("utf-8") comment = re.findall(rx.rule_comment.format(str(id)), src, re.X|re.M) if comment: comment = re.sub("^\#\s?", "", comment[0], re.M) return comment break return "No documentation comment present" # scan for APACHE_ENV= vars def read_env_vars(): for fn in tmp.env_locations: if os.path.exists(fn): src = open(fn, "r").read() tmp.env.update(dict(re.findall("""^\s*(?:export\s+)?([A-Z_]+)=["']?([\w/\-.]+)""", src, re.M))) # iterate over all Apache config files, visit relevant ones (vhosts/mod_security configs) def scan_all(): read_env_vars() for fn in apache_dump_includes(): src = open(fn, "r").read() #.decode("utf-8") if rx.interesting.search(src): vhosts[fn] = vhost(fn, src) # get *.conf list from apache2ctl def apache_dump_includes(): stdout = subprocess.Popen(["apache2ctl", "-t", "-D", "DUMP_INCLUDES"], stdout=subprocess.PIPE).stdout return rx.dump_includes.findall(stdout.read().decode("utf-8")) # just used once def count_tags(): import collections tags = [] for fn,v in rules.items(): if v.tags: tags = tags + v.tags print(list(reversed(list(collections.Counter(tags).keys())))) # prepare list of names for mainwindow vhosts/conf combobox def list_vhosts(types=["cfg","vhost"]): return [k for k,v in vhosts.items() if v.t in types] # initialization (scan_all) is done atop mainwindow #scan_all() #count_tags() #pprint(vhosts) #print({k:pprint(v.__dict__) for k,v in vhosts.items()}) |
Added modseccfg/writer.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | # encoding: utf-8 # api: modseccfg # title: Writer # description: updates *.conf files with new directives # version: 0.1 # type: file # category: config # config: # { name: write_etc, type: bool, value: 0, description: "Write to /etc without extra warnings", help: "Normally modseccfg would not update default apache/modsecurity config files." } # { name: write_sudo, type: bool, value: 0, description: "Use sudo to update non-writable files", help: "Run `sudo` on commandline to update files, if permissions insufficient" } # state: alpha # # Reads, updates and then writes back configuration files. # Contains multiple functions for different directives. # Some need replacing, while others (lists) just need to be # appended. # import os, re from modseccfg import vhosts, appsettings, utils import PySimpleGUI as sg class rx: pfx = re.compile(""" ^(\s*)\w+ """, re.M) end = re.compile(""" \Z | ^\s*</VirtualHost> """, re.M) # read src from config file def read(fn): return open(fn, "r", encoding="utf8").read() # update file def write(fn, src): if not appsettings.conf["write_etc"] and re.search("^/etc/|^/usr/share/", fn):# and not re.search("/sites|/crs-setup.conf", fn): # alternatively check for `#editable:1` header with pluginconf if sg.popup_yes_no("Default Apache/mod_sec config file '{}' should not be updated. Proceed anyway?".format(fn)) != "Yes": return if not os.access(fn, os.W_OK): sg.popup_cancel("Config file '{}' isn't writeable. (Use chown/chmod to make it so.)".format(fn)) # elif appsettings.conf["write_sudo"]: write_sudo(fn, src) return open(fn, "w", encoding="utf8").write(src) # write to file via sudo/tee pipe instead def write_sudo(fn, src): p = subprocess.Popen(['sudo', 'tee'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) p.stdin.write(src.encode("utf-8")) p.stdin.close() print(p.stdout.read()) p.wait() # detect leading whitespace def pfx(src): space = rx.pfx.findall(src) if space: return space[0] else: return "" # updates `src` on existing occurence of directive, else appends def augment(src, directive, value): pass # doesn't look for context def append(fn, directive, value): src = read(fn) src = rx.end.sub("{} {}".format(directive, value), src) write(fn, src) |
Added requirements.txt.
> > > | 1 2 3 | pysimplegui python3-tk pluginconf |
Added setup.py.
> > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #!/usr/bin/env python3 # encoding: utf-8 # api: pip # type: build # title: config for setuptools # # Always prefer setuptools over distutils # from pluginconf.setup import setup setup( fn="modseccfg/__init__.py", name="modseccfg", package_dir={"": "."}, package_data={}, data_files=[], entry_points={ "console_scripts": [ "modseccfg=modseccfg.mainwindow:main", ] } ) |