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
#!/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





>

<







1
2
3
4
5
6
7

8
9
10
11
12
13
14
#!/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
31
32
33
34
35
36
37
38
39
	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








|

31
32
33
34
35
36
37
38
39
	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
# 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






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
                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)
                ],







|







65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
                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)
                ],
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
    [
        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"),







|
|
|
|







113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
    [
        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"),
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
            # 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)







|





|







324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
            # 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)
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
        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()







|







440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
        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

        # 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",







|







196
197
198
199
200
201
202
203
204
205
206
207
208
209
210

        # 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",
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
            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)"







|







241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
            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

        # 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{}







|







206
207
208
209
210
211
212
213
214
215
216
217
218
219
220

        # 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{}
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340

    # 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







|

|







324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340

    # 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
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
    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)
        







|







360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
    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)