GUI editor to tame mod_security rules

โŒˆโŒ‹ โŽ‡ branch:  modseccfg


Check-in [5f05a5d785]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Remove remaining emoji Unicode occurences (info, modify, vhosts)
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 5f05a5d785ab5532c0eda6f0f06b50854b69cbf4dc2a049fb7bf461be4c40c4a
User & Date: mario 2021-02-24 20:16:44
Context
2021-02-25
13:19
Removed Wrap and Masquerade menu entries. check-in: c563db9866 user: mario tags: trunk
2021-02-24
20:16
Remove remaining emoji Unicode occurences (info, modify, vhosts) check-in: 5f05a5d785 user: mario tags: trunk
2021-02-02
16:07
Move to safer Unicode glyphs. Tk doesn't like current system setup: <blockquote> X Error of failed request: BadLength (poly request too large or internal Xlib length error) Major opcode of failed request: 139 (RENDER) Minor opcode of failed request: 20 (RenderAddGlyphs) Serial number of failed request: 28016 Current serial number in output stream: 28018 </blockquote> check-in: acaec56692 user: mario tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to Makefile.

1
2
3
4
5

6
7
8
9
10
11
12
13
14
..
31
32
33
34
35
36
37
38
39
#!/usr/bin/make

run:
	python3 -m modseccfg
setup:

	pandoc README.md -o README.rst
	version --read modseccfg/__init__.py --write modseccfg/mainwindow.py
	python3 setup.py bdist_wheel
	rm -r modseccfg.egg-info
upload: setup
	python3 setup.py bdist_wheel upload
	rm -r modseccfg.egg-info
t:
	pytest -v -v -v -v
................................................................................
	cd logfmt1 ; PYTHONPATH=. mkdocs build -v -v -v
	sed -i 's/table\.docutils/table/g' logfmt1/html/css/theme.css 
	sed -i 's/[{};]/&\n/g' logfmt1/html/css/theme.css
%.1:	%.md
	pandoc --standalone -f markdown+pandoc_title_block -t man $< -o $@
%.5:	%.md
	pandoc --standalone -f markdown+pandoc_title_block -t man $< -o $@
man:	logfmt1/manpage/logex.1 logfmt1/manpage/update-logfmt.1 logfmt1/manpage/logfmt.5






>

<







 







|

1
2
3
4
5
6
7

8
9
10
11
12
13
14
..
31
32
33
34
35
36
37
38
39
#!/usr/bin/make

run:
	python3 -m modseccfg
setup:
	version --read modseccfg/__init__.py --write modseccfg/mainwindow.py --write:_raw_ manpage/*md
	pandoc README.md -o README.rst

	python3 setup.py bdist_wheel
	rm -r modseccfg.egg-info
upload: setup
	python3 setup.py bdist_wheel upload
	rm -r modseccfg.egg-info
t:
	pytest -v -v -v -v
................................................................................
	cd logfmt1 ; PYTHONPATH=. mkdocs build -v -v -v
	sed -i 's/table\.docutils/table/g' logfmt1/html/css/theme.css 
	sed -i 's/[{};]/&\n/g' logfmt1/html/css/theme.css
%.1:	%.md
	pandoc --standalone -f markdown+pandoc_title_block -t man $< -o $@
%.5:	%.md
	pandoc --standalone -f markdown+pandoc_title_block -t man $< -o $@
man:	manpage/modseccfg.1 logfmt1/manpage/logex.1 logfmt1/manpage/update-logfmt.1 logfmt1/manpage/logfmt.5

Changes to manpage/modseccfg.1.

1
2
3
4
5
6
7
8
9
10
.\" Automatically generated by Pandoc 2.5
.\"
.TH "modseccfg" "1" "" "modseccfg and logfmt1 utilities" "Version 0.7.0"
.hy
.SH NAME
.PP
\f[B]modseccfg\f[R] \[em] GUI editor for mod_security rules on Apache
setups
.SH SYNOPSIS
.PP


|







1
2
3
4
5
6
7
8
9
10
.\" Automatically generated by Pandoc 2.5
.\"
.TH "modseccfg" "1" "" "modseccfg and logfmt1 utilities" "Version 0.7.3"
.hy
.SH NAME
.PP
\f[B]modseccfg\f[R] \[em] GUI editor for mod_security rules on Apache
setups
.SH SYNOPSIS
.PP

Changes to manpage/modseccfg.md.

1
2
3
4
5
6
7
8
% modseccfg(1) modseccfg and logfmt1 utilities | Version 0.7.0


NAME
====

**modseccfg** โ€” GUI editor for mod_security rules on Apache setups

|







1
2
3
4
5
6
7
8
% modseccfg(1) modseccfg and logfmt1 utilities | Version 0.7.3


NAME
====

**modseccfg** โ€” GUI editor for mod_security rules on Apache setups

Changes to modseccfg/__init__.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
# encoding: utf-8
# api: python
# type: init
# title: modseccfg
# description: Editor to tame mod_security rulesets
# version: 0.7.0
# state:   prototype
# support: none
# license: Apache-2.0
# depends: python:pysimplegui (>= 3.0), python:pluginconf (>= 0.7.3),
#    python:appdirs (>= 1.3), python:logfmt1 (>= 0.4),
#    python (>= 3.6), deb:python3-tk, bin:sshfs
# priority: core





|







1
2
3
4
5
6
7
8
9
10
11
12
13
# encoding: utf-8
# api: python
# type: init
# title: modseccfg
# description: Editor to tame mod_security rulesets
# version: 0.7.3
# state:   prototype
# support: none
# license: Apache-2.0
# depends: python:pysimplegui (>= 3.0), python:pluginconf (>= 0.7.3),
#    python:appdirs (>= 1.3), python:logfmt1 (>= 0.4),
#    python (>= 3.6), deb:python3-tk, bin:sshfs
# priority: core

Changes to modseccfg/install/setup_preconf_scheme.py.

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
        confn = fn
if not confn:
    print("Couldn't find 900-EXCLUSION.conf")
else:

    # run recipe window
    if vhosts.tmp.decl_preconf:
        print("โœ” *.preconf includes already configured")
    else:
        print("\n# IncludeOptional .../*.preconf")
        src = recipe.template_funcs.crs_preconfig(vars={}, data={"confn":confn})
        writer.append(fn=confn, directive=src, value="", comment="")
        vhosts.tmp.decl_preconf = True
        print(f"โ†’ updated: {confn}")

    # conf.preconf
    if conf.preconf:
        print("โœ” preconf use already activated")
    else:
        print("# enable ๐Ÿ—น preconf usage globally")
        utils.conf.preconf = True
        utils.cfg_write()
        print(f"โ†’ updated: {conf.conf_dir}/{conf.conf_file}")








|









|






20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
        confn = fn
if not confn:
    print("Couldn't find 900-EXCLUSION.conf")
else:

    # run recipe window
    if vhosts.tmp.decl_preconf:
        print("๐Ÿ—ธ *.preconf includes already configured")
    else:
        print("\n# IncludeOptional .../*.preconf")
        src = recipe.template_funcs.crs_preconfig(vars={}, data={"confn":confn})
        writer.append(fn=confn, directive=src, value="", comment="")
        vhosts.tmp.decl_preconf = True
        print(f"โ†’ updated: {confn}")

    # conf.preconf
    if conf.preconf:
        print("๐Ÿ—ธ preconf use already activated")
    else:
        print("# enable ๐Ÿ—น preconf usage globally")
        utils.conf.preconf = True
        utils.cfg_write()
        print(f"โ†’ updated: {conf.conf_dir}/{conf.conf_file}")

Changes to modseccfg/mainwindow.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
...
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
...
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
...
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# encoding: utf-8
# api: python
# type: main
# title: main window
# description: GUI with menus, actions, rules and logs
# category: config
# version: 0.7.0
# state:   alpha
# license: Apache-2.0
# config: 
#    { name: theme, type: select, value: DefaultNoMoreNagging, 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." }
#    { name: switch_auto, type: bool, value: 0, description: "Automatically switch to matching error.log when selecting vhost" }
#    { name: keyboard_binds, type: bool, value: 1, description: "Enable keyboard shortcuts in main window", help: "F1=info, F3/F4=editor, F5=log-viewer, F12=settings" }
# priority: core
................................................................................
                continue
            parent = ""
            if r.chained_to:
                parent = r.chained_to
                if parent in hidden:
                    continue
            # prepare treedata attributes
            state = rulestate.get(id, "๐Ÿ—ธ")  # formerly: -1=โž—, 0=โŒ, 1=๏“๏”, undef=โœ…
            rule_tree.insert(
                parent=parent,
                key=id,
                text=id,
                values=[
                   state, str(id), r.msg, r.tag_primary, log_count.get(id, 0)
                ],
................................................................................
    [
        sg.Column([
            # menu
            [sg.Menu(menu, key="menu")],
            # button row
            [
                sg.Button("๐Ÿ›ˆ Info", tooltip="SecRule details"),#โญ
                sg.Button("๐Ÿ—ถ Disable", tooltip="SecRuleRemoveById"),#โŒ
                sg.Button("๐Ÿ—ธ Enable", tooltip="remove SecRuleRemove"),#โœ…
                sg.Button("โ‹‡ Modify", tooltip="SecRuleUpdateAction/Target", disabled=0),#โž—
                sg.ButtonMenu("โ‹š Wrap", ["Wrap",["<FilesMatch>","<Location>","<Directory>"]], disabled=0, k="menu_wrap"),#โฎโฏ
                sg.T(" " * 18),
                sg.Button("Filter", key="filter_log", button_color=("white","gray"), font="Sans 10", tooltip="Apply filter phrase to current log"),
                sg.Combo(values=["", "injection", "500|429", "bot"], size=(20,1), key="log_filter", enable_events=True, tooltip="Regex to filter with")
            ],
            [sg.T("  "*71+"โ†“")],
            # comboboxes
            [sg.Text("vhost/conf", font="bold"),
................................................................................
            # Clean up comment a little (comments aren't strictly speaking allowed,
            # but mod_security effectively proccesses them and simply ignores any
            # trailing garbage. So we just need to ensure there aren't any extra
            # integers to be interpreted as RemoveById rule numbers.
            comment = re.sub("[^\w\s,:./]", "", vhosts.rules[self.id].msg)  # retain just a bit of text
            comment = re.sub("(\d)", lambda m: chr(0xFEE0 + ord(m.group(1))), comment) # integers to unicode glyphs
            writer.append(data["confn"], directive="SecRuleRemoveById", value=self.id, comment=" # "+comment)
            self._update_rulestate(self.id, "โŒ")

    # remove any "SecRuleRemove* {id}" in vhost.conf
    @ui.needs_id
    @ui.needs_confn
    def enable(self, data):
        if self.vh and self.vh.rulestate.get(self.id) != "โŒ" and self._cancel("SecRule might be wrapped/masked. Reenable anyway?"):
            return
        writer.remove_remove(data["confn"], "SecRuleRemoveById", self.id)
        self._update_rulestate(self.id, None)

    # File: Settings - remapped to pluginconf window
    def settings(self, data):
        utils.cfg_window(self)
................................................................................
        self.win_register(w, lambda *x: None)

    # renew display of ruletree with current log and vhost rulestate
    def _update_rules(self, *data):
        if self.vh:
            self.w["rule"].update(ui.rules(log_count=logs.log_count, rulestate=self.vh.rulestate))

    # called from disable/enable to set โŒ=disabled, โž—=modified, ๏“๏”=wrapped, โœ…/None=enabled, etc
    def _update_rulestate(self, id, val):
        if self.vh:
            if val==None and id in self.vh.rulestate:
                del self.vh.rulestate[id]
            else:
                self.vh.rulestate[id] = val
            self._update_rules()






|







 







|







 







|
|
|
|







 







|





|







 







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
..
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
...
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
...
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
...
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# encoding: utf-8
# api: python
# type: main
# title: main window
# description: GUI with menus, actions, rules and logs
# category: config
# version: 0.7.3
# state:   alpha
# license: Apache-2.0
# config: 
#    { name: theme, type: select, value: DefaultNoMoreNagging, 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." }
#    { name: switch_auto, type: bool, value: 0, description: "Automatically switch to matching error.log when selecting vhost" }
#    { name: keyboard_binds, type: bool, value: 1, description: "Enable keyboard shortcuts in main window", help: "F1=info, F3/F4=editor, F5=log-viewer, F12=settings" }
# priority: core
................................................................................
                continue
            parent = ""
            if r.chained_to:
                parent = r.chained_to
                if parent in hidden:
                    continue
            # prepare treedata attributes
            state = rulestate.get(id, "๐Ÿ—ธ")  # ๐Ÿ—ถ=disabled, โ‹‡=modified, โ‹š=wrapped, ๐Ÿ—ธ/None=enabled, formerly: -1=โž—, 0=โŒ, 1=๏“๏”, undef=โœ…
            rule_tree.insert(
                parent=parent,
                key=id,
                text=id,
                values=[
                   state, str(id), r.msg, r.tag_primary, log_count.get(id, 0)
                ],
................................................................................
    [
        sg.Column([
            # menu
            [sg.Menu(menu, key="menu")],
            # button row
            [
                sg.Button("๐Ÿ›ˆ Info", tooltip="SecRule details"),#โญ
                sg.Button("๐Ÿ—ถ Disable", tooltip="SecRuleRemoveById"),
                sg.Button("๐Ÿ—ธ Enable", tooltip="remove SecRuleRemove"),
                sg.Button("โ‹‡ Modify", tooltip="SecRuleUpdateAction/Target", disabled=0),
                sg.ButtonMenu("โ‹š Wrap", ["Wrap",["<FilesMatch>","<Location>","<Directory>"]], disabled=0, k="menu_wrap"),
                sg.T(" " * 18),
                sg.Button("Filter", key="filter_log", button_color=("white","gray"), font="Sans 10", tooltip="Apply filter phrase to current log"),
                sg.Combo(values=["", "injection", "500|429", "bot"], size=(20,1), key="log_filter", enable_events=True, tooltip="Regex to filter with")
            ],
            [sg.T("  "*71+"โ†“")],
            # comboboxes
            [sg.Text("vhost/conf", font="bold"),
................................................................................
            # Clean up comment a little (comments aren't strictly speaking allowed,
            # but mod_security effectively proccesses them and simply ignores any
            # trailing garbage. So we just need to ensure there aren't any extra
            # integers to be interpreted as RemoveById rule numbers.
            comment = re.sub("[^\w\s,:./]", "", vhosts.rules[self.id].msg)  # retain just a bit of text
            comment = re.sub("(\d)", lambda m: chr(0xFEE0 + ord(m.group(1))), comment) # integers to unicode glyphs
            writer.append(data["confn"], directive="SecRuleRemoveById", value=self.id, comment=" # "+comment)
            self._update_rulestate(self.id, "๐Ÿ—ถ")

    # remove any "SecRuleRemove* {id}" in vhost.conf
    @ui.needs_id
    @ui.needs_confn
    def enable(self, data):
        if self.vh and self.vh.rulestate.get(self.id) != "๐Ÿ—ถ" and self._cancel("SecRule might be wrapped/masked. Reenable anyway?"):
            return
        writer.remove_remove(data["confn"], "SecRuleRemoveById", self.id)
        self._update_rulestate(self.id, None)

    # File: Settings - remapped to pluginconf window
    def settings(self, data):
        utils.cfg_window(self)
................................................................................
        self.win_register(w, lambda *x: None)

    # renew display of ruletree with current log and vhost rulestate
    def _update_rules(self, *data):
        if self.vh:
            self.w["rule"].update(ui.rules(log_count=logs.log_count, rulestate=self.vh.rulestate))

    # called from disable/enable to set ๐Ÿ—ถ=disabled, โ‹‡=modified, โ‹š=wrapped, ๐Ÿ—ธ/None=enabled, etc
    def _update_rulestate(self, id, val):
        if self.vh:
            if val==None and id in self.vh.rulestate:
                del self.vh.rulestate[id]
            else:
                self.vh.rulestate[id] = val
            self._update_rules()

Changes to modseccfg/modify.py.

196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
...
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255

        # window
        layout = [
            [sg.Menu([[f"Rule {id}",["Info", "Save", "Close"]]], key="menu")],
            [sg.Column(layout, expand_x=1, expand_y=0, size=(635,720), scrollable="no", element_justification='left')],
            [sg.Button("Save"), sg.Button("Cancel")]
        ]
        self.w = sg.Window(layout=layout, title=f"โž— SecRuleUpdate #{id}", resizable=1, font="Sans 12", icon=icons.icon)
        mainwindow.win_register(self.w, self.event)
    

    # widget: flags[] checkbox
    def w_flag(self, name, radio=0):
        kw = {
            "text": f"{name} ", "key": f"flags[{name}]", "disabled": name == "chain",
................................................................................
            sg.Combo([""] + ["!"+name for name in show.targets], k=f"vars[{i}]", default_value=val, size=(30,1), enable_events=True, disabled=self.r.vars=="@SecAction"),
            sg.T("!exclude or add", text_color="gray")
        ]

    # widget: help link
    def w_help(self, anchor, **kw):
        help = dict(text_color="#bbd", pad=[(0,0),(12,0)], enable_events=True); help.update(kw)
        return sg.T("โชโ“โซ", key="https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-%28v2.x%29#"+anchor, **help)

    # description text
    def t_target_desc(self, r):
        if isinstance(r.id, float):
            return "(CHAINED rule!)"
        elif r.vars == "@SecAction":
            return "(Faux rule)"







|







 







|







196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
...
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255

        # window
        layout = [
            [sg.Menu([[f"Rule {id}",["Info", "Save", "Close"]]], key="menu")],
            [sg.Column(layout, expand_x=1, expand_y=0, size=(635,720), scrollable="no", element_justification='left')],
            [sg.Button("Save"), sg.Button("Cancel")]
        ]
        self.w = sg.Window(layout=layout, title=f"โ‹‡ SecRuleUpdate #{id}", resizable=1, font="Sans 12", icon=icons.icon)
        mainwindow.win_register(self.w, self.event)
    

    # widget: flags[] checkbox
    def w_flag(self, name, radio=0):
        kw = {
            "text": f"{name} ", "key": f"flags[{name}]", "disabled": name == "chain",
................................................................................
            sg.Combo([""] + ["!"+name for name in show.targets], k=f"vars[{i}]", default_value=val, size=(30,1), enable_events=True, disabled=self.r.vars=="@SecAction"),
            sg.T("!exclude or add", text_color="gray")
        ]

    # widget: help link
    def w_help(self, anchor, **kw):
        help = dict(text_color="#bbd", pad=[(0,0),(12,0)], enable_events=True); help.update(kw)
        return sg.T("(?)", key="https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-%28v2.x%29#"+anchor, **help)

    # description text
    def t_target_desc(self, r):
        if isinstance(r.id, float):
            return "(CHAINED rule!)"
        elif r.vars == "@SecAction":
            return "(Faux rule)"

Changes to modseccfg/ruleinfo.py.

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
    # rule lookup
    r = vhosts.rules[id]  # SecRule
    is_virt = ""
    if type(id) is float:
        is_virt = "(virtual id/chained rule)"
    decl_vh = r.vhost()
    file_url = "file://"+decl_vh.fn
    is_state = "โœ…"
    if decl_vh and decl_vh.rulestate.get(id):
        is_state = decl_vh.rulestate[id] + " in rules.conf"  # conditional SecRule declaration
    elif vh and vh.rulestate.get(id):
        is_state = vh.rulestate[id]

    # params 2 widget
    layout = [
        [
            # SecRule #123456
            sg.T(f"SecRule {id}", **style.head),
            # (virtual id)
            sg.T(is_virt, **style.virt),
            #  โž— โŒ ๏“๏” undef=โœ…
            sg.T(f"state={is_state}", **style.state)
        ],
        [
            # rule comment
            sg.Frame("doc", size=(90,4), layout=[
               [sg.T(file_url, key=file_url, **style.link)],
               [sg.Multiline(r.help(), auto_size_text=1, size=(60,4), background_color="lightgray")],







|












|







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
    # rule lookup
    r = vhosts.rules[id]  # SecRule
    is_virt = ""
    if type(id) is float:
        is_virt = "(virtual id/chained rule)"
    decl_vh = r.vhost()
    file_url = "file://"+decl_vh.fn
    is_state = "๐Ÿ—ธ"
    if decl_vh and decl_vh.rulestate.get(id):
        is_state = decl_vh.rulestate[id] + " in rules.conf"  # conditional SecRule declaration
    elif vh and vh.rulestate.get(id):
        is_state = vh.rulestate[id]

    # params 2 widget
    layout = [
        [
            # SecRule #123456
            sg.T(f"SecRule {id}", **style.head),
            # (virtual id)
            sg.T(is_virt, **style.virt),
            #  ๐Ÿ—ถ  โ‹‡  โ‹š  ๐Ÿ—ธ
            sg.T(f"state={is_state}", **style.state)
        ],
        [
            # rule comment
            sg.Frame("doc", size=(90,4), layout=[
               [sg.T(file_url, key=file_url, **style.link)],
               [sg.Multiline(r.help(), auto_size_text=1, size=(60,4), background_color="lightgray")],

Changes to modseccfg/vhosts.py.

206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
...
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
...
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374

        # vhost properties
        self.fn = fn
        self.t = "cfg"
        self.name = ""
        self.logs = []
        self.cfg = {}
        self.rulestate = {}    # โž— โŒ ๏“๏” undef=โœ…
        self.ruledecl = {}
        self.update = {}       # SecRuleUpdateโ€ฆ map
        self.warn = ""

        # internal state
        self.linemap = {}      # lineno โ†’ first id: occurence
        self.mk_linemap(src)   # fill .linemap{}
................................................................................

    # modsec: just a secrule without conditions
    def secaction(self, args):
        self.secrule(["@SecAction", "setvar:", args[0]])
    
    # modsec: SecRuleRemoveById 900001 900002 900003
    def secruleremovebyid(self, args):
        state = "๐Ÿ—ถ" #"โŒ"
        if self.wrap:  # record if within <Dir|File|If|Wrap> section
            state = "โ‹š" #"๏“๏”"
            #log.info("wrapped SecRuleRm", self.fn, self.wrap, 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] = state
................................................................................
    def _secruleupdate(self, cls, id, arg, *repl):
        if re.match("^\d+:\d$", id):
            id = float(id.replace(":", "."))
        elif re.match("^\d+$", id):
            id = int(id)
        else:
            return
        self.rulestate[id] = "โ‹‡" #"โž—"
        # We don't really use the detail. This is just to record that any one rule has been "modified".
        if repl:
            arg = f"!{repl[0]},{arg}"  # merge third parameter from `SecRuleUpdateTarget 123456 NEW_TARGET REMOVE_VAR`
        if not self.update.get(id):
            self.update[id] = {"vars":[], "actions":[]}
        self.update[id][cls].append(arg)
        







|







 







|

|







 







|







206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
...
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
...
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374

        # vhost properties
        self.fn = fn
        self.t = "cfg"
        self.name = ""
        self.logs = []
        self.cfg = {}
        self.rulestate = {}    # ๐Ÿ—ถ=disabled, โ‹‡=modified, โ‹š=wrapped, ๐Ÿ—ธ=enabled
        self.ruledecl = {}
        self.update = {}       # SecRuleUpdateโ€ฆ map
        self.warn = ""

        # internal state
        self.linemap = {}      # lineno โ†’ first id: occurence
        self.mk_linemap(src)   # fill .linemap{}
................................................................................

    # modsec: just a secrule without conditions
    def secaction(self, args):
        self.secrule(["@SecAction", "setvar:", args[0]])
    
    # modsec: SecRuleRemoveById 900001 900002 900003
    def secruleremovebyid(self, args):
        state = "๐Ÿ—ถ"
        if self.wrap:  # record if within <Dir|File|If|Wrap> section
            state = "โ‹š"
            #log.info("wrapped SecRuleRm", self.fn, self.wrap, 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] = state
................................................................................
    def _secruleupdate(self, cls, id, arg, *repl):
        if re.match("^\d+:\d$", id):
            id = float(id.replace(":", "."))
        elif re.match("^\d+$", id):
            id = int(id)
        else:
            return
        self.rulestate[id] = "โ‹‡"
        # We don't really use the detail. This is just to record that any one rule has been "modified".
        if repl:
            arg = f"!{repl[0]},{arg}"  # merge third parameter from `SecRuleUpdateTarget 123456 NEW_TARGET REMOVE_VAR`
        if not self.update.get(id):
            self.update[id] = {"vars":[], "actions":[]}
        self.update[id][cls].append(arg)