sec-audit-rules 0.1.0a2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. sec_audit_rules-0.1.0a2/.gitignore +216 -0
  2. sec_audit_rules-0.1.0a2/LICENSE +21 -0
  3. sec_audit_rules-0.1.0a2/PKG-INFO +92 -0
  4. sec_audit_rules-0.1.0a2/README.md +57 -0
  5. sec_audit_rules-0.1.0a2/pyproject.toml +70 -0
  6. sec_audit_rules-0.1.0a2/src/sec_audit/enforcement/__init__.py +41 -0
  7. sec_audit_rules-0.1.0a2/src/sec_audit/enforcement/actions.py +88 -0
  8. sec_audit_rules-0.1.0a2/src/sec_audit/enforcement/blocks.py +62 -0
  9. sec_audit_rules-0.1.0a2/src/sec_audit/enforcement/config.py +99 -0
  10. sec_audit_rules-0.1.0a2/src/sec_audit/enforcement/policies.py +65 -0
  11. sec_audit_rules-0.1.0a2/src/sec_audit/integrations/__init__.py +1 -0
  12. sec_audit_rules-0.1.0a2/src/sec_audit/integrations/wazuh/__init__.py +3 -0
  13. sec_audit_rules-0.1.0a2/src/sec_audit/integrations/wazuh/api.py +69 -0
  14. sec_audit_rules-0.1.0a2/src/sec_audit/integrations/wazuh/rules/0375-sec-audit.xml +7 -0
  15. sec_audit_rules-0.1.0a2/src/sec_audit/integrations/wazuh/rules/sigma/audit-rule-match.yml +8 -0
  16. sec_audit_rules-0.1.0a2/src/sec_audit/rules/__init__.py +50 -0
  17. sec_audit_rules-0.1.0a2/src/sec_audit/rules/base.py +228 -0
  18. sec_audit_rules-0.1.0a2/src/sec_audit/rules/builtins/__init__.py +16 -0
  19. sec_audit_rules-0.1.0a2/src/sec_audit/rules/builtins/brute_force.py +87 -0
  20. sec_audit_rules-0.1.0a2/src/sec_audit/rules/builtins/model_changes.py +62 -0
  21. sec_audit_rules-0.1.0a2/src/sec_audit/rules/builtins/proxy.py +69 -0
  22. sec_audit_rules-0.1.0a2/src/sec_audit/rules/builtins/repeated_errors.py +39 -0
  23. sec_audit_rules-0.1.0a2/src/sec_audit/rules/builtins/request_body.py +69 -0
  24. sec_audit_rules-0.1.0a2/src/sec_audit/rules/builtins/routes.py +74 -0
  25. sec_audit_rules-0.1.0a2/src/sec_audit/rules/config.py +44 -0
  26. sec_audit_rules-0.1.0a2/src/sec_audit/rules/engine.py +225 -0
  27. sec_audit_rules-0.1.0a2/src/sec_audit/rules/events.py +374 -0
  28. sec_audit_rules-0.1.0a2/src/sec_audit/rules/history.py +155 -0
  29. sec_audit_rules-0.1.0a2/src/sec_audit/rules/result_sinks/__init__.py +1 -0
  30. sec_audit_rules-0.1.0a2/src/sec_audit/rules/scopes.py +147 -0
  31. sec_audit_rules-0.1.0a2/src/sec_audit/rules/stores/__init__.py +32 -0
  32. sec_audit_rules-0.1.0a2/src/sec_audit/rules/stores/counters.py +159 -0
  33. sec_audit_rules-0.1.0a2/src/sec_audit/rules/stores/history.py +106 -0
  34. sec_audit_rules-0.1.0a2/src/sec_audit/rules/stores/memory.py +4 -0
  35. sec_audit_rules-0.1.0a2/src/sec_audit/rules/stores/redis.py +223 -0
  36. sec_audit_rules-0.1.0a2/tests/test_context_rules.py +403 -0
  37. sec_audit_rules-0.1.0a2/tests/test_enforcement.py +93 -0
  38. sec_audit_rules-0.1.0a2/tests/test_redis_stores.py +99 -0
  39. sec_audit_rules-0.1.0a2/tests/test_rules.py +811 -0
  40. sec_audit_rules-0.1.0a2/tests/test_scope_registry.py +55 -0
  41. sec_audit_rules-0.1.0a2/tests/test_wazuh_assets.py +10 -0
@@ -0,0 +1,216 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ logs/*
61
+ !logs/.gitkeep
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ .pybuilder/
78
+ target/
79
+
80
+ # Jupyter Notebook
81
+ .ipynb_checkpoints
82
+
83
+ # IPython
84
+ profile_default/
85
+ ipython_config.py
86
+
87
+ # pyenv
88
+ # For a library or package, you might want to ignore these files since the code is
89
+ # intended to run in multiple environments; otherwise, check them in:
90
+ # .python-version
91
+
92
+ # pipenv
93
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
95
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
96
+ # install all needed dependencies.
97
+ #Pipfile.lock
98
+
99
+ # UV
100
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
102
+ # commonly ignored for libraries.
103
+ #uv.lock
104
+
105
+ # poetry
106
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110
+ #poetry.lock
111
+ #poetry.toml
112
+
113
+ # pdm
114
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
115
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
116
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
117
+ #pdm.lock
118
+ #pdm.toml
119
+ .pdm-python
120
+ .pdm-build/
121
+
122
+ # pixi
123
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
124
+ #pixi.lock
125
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
126
+ # in the .venv directory. It is recommended not to include this directory in version control.
127
+ .pixi
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule
134
+ celerybeat.pid
135
+
136
+ # SageMath parsed files
137
+ *.sage.py
138
+
139
+ # Environments
140
+ .env
141
+ .envrc
142
+ .venv
143
+ env/
144
+ venv/
145
+ ENV/
146
+ env.bak/
147
+ venv.bak/
148
+
149
+ # Spyder project settings
150
+ .spyderproject
151
+ .spyproject
152
+
153
+ # Rope project settings
154
+ .ropeproject
155
+
156
+ # mkdocs documentation
157
+ /site
158
+
159
+ # mypy
160
+ .mypy_cache/
161
+ .dmypy.json
162
+ dmypy.json
163
+
164
+ # Pyre type checker
165
+ .pyre/
166
+
167
+ # pytype static type analyzer
168
+ .pytype/
169
+
170
+ # Cython debug symbols
171
+ cython_debug/
172
+
173
+ # Celery beat schedule files
174
+ celerybeat-schedule
175
+ celerybeat.pid
176
+ celerybeat-schedule.db
177
+
178
+ # PyCharm
179
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
180
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
181
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
182
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
183
+ #.idea/
184
+
185
+ # Abstra
186
+ # Abstra is an AI-powered process automation framework.
187
+ # Ignore directories containing user credentials, local state, and settings.
188
+ # Learn more at https://abstra.io/docs
189
+ .abstra/
190
+
191
+ # Visual Studio Code
192
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
193
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
194
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
195
+ # you could uncomment the following to ignore the entire vscode folder
196
+ # .vscode/
197
+
198
+ # Ruff stuff:
199
+ .ruff_cache/
200
+
201
+ # PyPI configuration file
202
+ .pypirc
203
+
204
+ # Cursor
205
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
206
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
207
+ # refer to https://docs.cursor.com/context/ignore-files
208
+ .cursorignore
209
+ .cursorindexingignore
210
+
211
+ # Marimo
212
+ marimo/_static/
213
+ marimo/_lsp/
214
+ __marimo__/
215
+
216
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ammar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: sec-audit-rules
3
+ Version: 0.1.0a2
4
+ Summary: Framework-free audit rules engine and enforcement policies
5
+ Project-URL: Homepage, https://github.com/ammar39/sec-audit
6
+ Project-URL: Repository, https://github.com/ammar39/sec-audit
7
+ Project-URL: Documentation, https://github.com/ammar39/sec-audit/tree/main/packages/sec-audit-rules
8
+ Project-URL: Changelog, https://github.com/ammar39/sec-audit/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/ammar39/sec-audit/issues
10
+ Author-email: Ammar <ammarwaleed@proton.me>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: audit,enforcement,rules,security,siem,wazuh
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Security
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: sec-audit<0.2,>=0.1.0a1
26
+ Provides-Extra: dev
27
+ Requires-Dist: fakeredis[lua]>=2.21; extra == 'dev'
28
+ Requires-Dist: pytest>=8; extra == 'dev'
29
+ Requires-Dist: ruff>=0.8; extra == 'dev'
30
+ Provides-Extra: redis
31
+ Requires-Dist: redis>=5.0; extra == 'redis'
32
+ Provides-Extra: wazuh
33
+ Requires-Dist: httpx>=0.27; extra == 'wazuh'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # sec-audit-rules
37
+
38
+ Framework-free audit **rules engine**, **enforcement policies**, and **SIEM integrations**
39
+ for the [`sec-audit`](https://github.com/ammar39/sec-audit/tree/main/packages/sec-audit) core.
40
+
41
+ This package is **Django-free** and shares the `sec_audit` namespace with its sibling
42
+ distributions.
43
+
44
+ ## Features
45
+
46
+ - **Rules** — pure, read-only detectors that evaluate an event and optionally return a
47
+ `RuleMatch`. All state (counters, history, clock, config) is injected via `RuleContext`,
48
+ so rules have no side effects.
49
+ - **Engine** — filters events by type, isolates rule exceptions, and enforces safety flags
50
+ (`safe_for_enforcement`) for pre-request blocking.
51
+ - **Enforcement** — policies that turn matches into alert/block decisions with persistent
52
+ block scopes.
53
+ - **Integrations** — Wazuh XML/YAML detection rules ship as package data (no runtime Wazuh
54
+ import required).
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ pip install sec-audit-rules
60
+ # with the Wazuh HTTP client extra:
61
+ pip install "sec-audit-rules[wazuh]"
62
+ ```
63
+
64
+ ## Writing a rule
65
+
66
+ ```python
67
+ from sec_audit.rules import Rule, RuleMatch
68
+
69
+ class TooManyFailedLogins(Rule):
70
+ name = 'too_many_failed_logins'
71
+ event_types = {'auth.login.failed'}
72
+ severity = 8
73
+ safe_for_enforcement = True
74
+
75
+ def evaluate(self, event, ctx):
76
+ srcip = str(event.fields.get('srcip') or '')
77
+ count = ctx.counters.incr(f'login_fail:{srcip}', ttl=300)
78
+ if count < 5:
79
+ return None
80
+ return RuleMatch(
81
+ self.name, self.severity, ctx.now,
82
+ f'Too many failed logins from {srcip}',
83
+ metadata={'count': count, 'srcip': srcip},
84
+ )
85
+ ```
86
+
87
+ Rules are pure detectors — no logging, DB writes, or external calls; `RuleMatch.metadata`
88
+ is immutable after creation.
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,57 @@
1
+ # sec-audit-rules
2
+
3
+ Framework-free audit **rules engine**, **enforcement policies**, and **SIEM integrations**
4
+ for the [`sec-audit`](https://github.com/ammar39/sec-audit/tree/main/packages/sec-audit) core.
5
+
6
+ This package is **Django-free** and shares the `sec_audit` namespace with its sibling
7
+ distributions.
8
+
9
+ ## Features
10
+
11
+ - **Rules** — pure, read-only detectors that evaluate an event and optionally return a
12
+ `RuleMatch`. All state (counters, history, clock, config) is injected via `RuleContext`,
13
+ so rules have no side effects.
14
+ - **Engine** — filters events by type, isolates rule exceptions, and enforces safety flags
15
+ (`safe_for_enforcement`) for pre-request blocking.
16
+ - **Enforcement** — policies that turn matches into alert/block decisions with persistent
17
+ block scopes.
18
+ - **Integrations** — Wazuh XML/YAML detection rules ship as package data (no runtime Wazuh
19
+ import required).
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install sec-audit-rules
25
+ # with the Wazuh HTTP client extra:
26
+ pip install "sec-audit-rules[wazuh]"
27
+ ```
28
+
29
+ ## Writing a rule
30
+
31
+ ```python
32
+ from sec_audit.rules import Rule, RuleMatch
33
+
34
+ class TooManyFailedLogins(Rule):
35
+ name = 'too_many_failed_logins'
36
+ event_types = {'auth.login.failed'}
37
+ severity = 8
38
+ safe_for_enforcement = True
39
+
40
+ def evaluate(self, event, ctx):
41
+ srcip = str(event.fields.get('srcip') or '')
42
+ count = ctx.counters.incr(f'login_fail:{srcip}', ttl=300)
43
+ if count < 5:
44
+ return None
45
+ return RuleMatch(
46
+ self.name, self.severity, ctx.now,
47
+ f'Too many failed logins from {srcip}',
48
+ metadata={'count': count, 'srcip': srcip},
49
+ )
50
+ ```
51
+
52
+ Rules are pure detectors — no logging, DB writes, or external calls; `RuleMatch.metadata`
53
+ is immutable after creation.
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,70 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sec-audit-rules"
7
+ version = "0.1.0a2"
8
+ description = "Framework-free audit rules engine and enforcement policies"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [{ name = "Ammar", email = "ammarwaleed@proton.me" }]
12
+ requires-python = ">=3.10"
13
+ keywords = ["security", "audit", "rules", "enforcement", "wazuh", "siem"]
14
+ dependencies = [
15
+ "sec-audit>=0.1.0a1,<0.2",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Security",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/ammar39/sec-audit"
32
+ Repository = "https://github.com/ammar39/sec-audit"
33
+ Documentation = "https://github.com/ammar39/sec-audit/tree/main/packages/sec-audit-rules"
34
+ Changelog = "https://github.com/ammar39/sec-audit/blob/main/CHANGELOG.md"
35
+ Issues = "https://github.com/ammar39/sec-audit/issues"
36
+
37
+ [project.optional-dependencies]
38
+ wazuh = ["httpx>=0.27"]
39
+ redis = ["redis>=5.0"]
40
+ dev = ["pytest>=8", "ruff>=0.8", "fakeredis[lua]>=2.21"]
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/sec_audit"]
44
+ only-include = [
45
+ "src/sec_audit/rules",
46
+ "src/sec_audit/enforcement",
47
+ "src/sec_audit/integrations/wazuh",
48
+ ]
49
+
50
+ [tool.hatch.build.targets.wheel.sources]
51
+ # src/sec_audit -> sec_audit (Python 3.3+ implicit namespace package shared with sec-audit and django-sec-audit)
52
+ "src/sec_audit" = "sec_audit"
53
+
54
+ [tool.hatch.build]
55
+ # Package data (XML/YAML monitoring assets) ship as artifacts.
56
+ artifacts = [
57
+ "src/sec_audit/integrations/wazuh/rules/**/*.xml",
58
+ "src/sec_audit/integrations/wazuh/rules/**/*.yml",
59
+ "src/sec_audit/integrations/wazuh/rules/**/*.yaml",
60
+ ]
61
+
62
+ [tool.ruff]
63
+ target-version = "py310"
64
+ line-length = 88
65
+
66
+ [tool.ruff.format]
67
+ quote-style = "single"
68
+
69
+ [tool.pytest.ini_options]
70
+ pythonpath = ["src", "."]
@@ -0,0 +1,41 @@
1
+ from sec_audit.enforcement.actions import (
2
+ ALERT_SEVERITY,
3
+ BLOCKING_ACTIONS,
4
+ DEFAULT_BLOCK_SCOPES,
5
+ PERSISTENT_ACTIONS,
6
+ TEMPORARY_ACTIONS,
7
+ RuleAction,
8
+ effective_action_ttl,
9
+ resolve_rule_action,
10
+ )
11
+ from sec_audit.enforcement.blocks import (
12
+ BlockEntry,
13
+ BlockScope,
14
+ BlockStore,
15
+ )
16
+ from sec_audit.enforcement.config import EnforcementAuditConfig
17
+ from sec_audit.enforcement.policies import (
18
+ EnforcementDecision,
19
+ EnforcementPolicy,
20
+ SeverityEnforcementPolicy,
21
+ highest_severity_match,
22
+ )
23
+
24
+ __all__ = [
25
+ 'ALERT_SEVERITY',
26
+ 'BLOCKING_ACTIONS',
27
+ 'BlockEntry',
28
+ 'BlockScope',
29
+ 'BlockStore',
30
+ 'DEFAULT_BLOCK_SCOPES',
31
+ 'EnforcementAuditConfig',
32
+ 'EnforcementDecision',
33
+ 'EnforcementPolicy',
34
+ 'PERSISTENT_ACTIONS',
35
+ 'RuleAction',
36
+ 'SeverityEnforcementPolicy',
37
+ 'TEMPORARY_ACTIONS',
38
+ 'effective_action_ttl',
39
+ 'highest_severity_match',
40
+ 'resolve_rule_action',
41
+ ]
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Mapping
5
+
6
+ from sec_audit.enforcement.policies import EnforcementDecision
7
+ from sec_audit.rules.base import RuleMatch
8
+
9
+ TEMPORARY_ACTIONS = {'temp_block'}
10
+ PERSISTENT_ACTIONS = {'persist_block'}
11
+ BLOCKING_ACTIONS = {'block', 'temp_block', 'persist_block'}
12
+ ALERT_SEVERITY = 4
13
+ DEFAULT_BLOCK_SCOPES = ('ip',)
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class RuleAction:
18
+ action: str
19
+ ttl: int | None = None
20
+ scopes: tuple[str, ...] = DEFAULT_BLOCK_SCOPES
21
+ status_code: int | None = None
22
+ message: str | None = None
23
+
24
+
25
+ def _configured_rule_action(spec: object) -> RuleAction:
26
+ if isinstance(spec, str):
27
+ return RuleAction(action=spec)
28
+ data = dict(spec) if isinstance(spec, Mapping) else {}
29
+ scopes = data.get('scopes') or DEFAULT_BLOCK_SCOPES
30
+ if isinstance(scopes, str):
31
+ scopes = (scopes,)
32
+ return RuleAction(
33
+ action=str(data.get('action') or 'observe'),
34
+ ttl=data.get('ttl'),
35
+ scopes=tuple(str(scope) for scope in scopes),
36
+ status_code=data.get('status_code'),
37
+ message=data.get('message'),
38
+ )
39
+
40
+
41
+ def _match_block_ttl(match: RuleMatch, default_ttl: int | None) -> int | None:
42
+ ttl = match.metadata.get('block_ttl', match.metadata.get('ttl'))
43
+ if ttl is None and match.decision == 'temp_block':
44
+ ttl = default_ttl or 300
45
+ return int(ttl) if ttl is not None else None
46
+
47
+
48
+ def effective_action_ttl(
49
+ rule_action: RuleAction,
50
+ match: RuleMatch,
51
+ default_ttl: int | None,
52
+ ) -> int | None:
53
+ return rule_action.ttl or _match_block_ttl(match, default_ttl) or default_ttl
54
+
55
+
56
+ def resolve_rule_action(
57
+ match: RuleMatch,
58
+ *,
59
+ configured_actions: Mapping[str, object],
60
+ block_rules: Mapping[str, int],
61
+ default_ttl: int | None,
62
+ policy_decision: EnforcementDecision | None = None,
63
+ default_action: str = 'observe',
64
+ ) -> RuleAction:
65
+ configured = configured_actions.get(match.rule_name)
66
+ if configured is not None:
67
+ return _configured_rule_action(configured)
68
+ if match.rule_name in block_rules:
69
+ return RuleAction(action='temp_block', ttl=block_rules[match.rule_name])
70
+ if match.decision in BLOCKING_ACTIONS or match.decision in {'alert', 'observe'}:
71
+ return RuleAction(
72
+ action=str(match.decision),
73
+ ttl=_match_block_ttl(match, default_ttl),
74
+ )
75
+ if policy_decision is not None and not policy_decision.allowed:
76
+ return RuleAction(
77
+ action='block',
78
+ status_code=policy_decision.status_code,
79
+ message=policy_decision.message,
80
+ )
81
+ if default_action == 'alert':
82
+ return RuleAction(action='alert')
83
+ if default_action in BLOCKING_ACTIONS:
84
+ return RuleAction(
85
+ action=str(default_action),
86
+ ttl=_match_block_ttl(match, default_ttl),
87
+ )
88
+ return RuleAction(action='observe')
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from types import MappingProxyType
6
+ from typing import Mapping, Protocol, Sequence
7
+
8
+ DEFAULT_BLOCK_MESSAGE = 'Request blocked by audit enforcement policy'
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class BlockScope:
13
+ scope_type: str
14
+ scope_value: str
15
+
16
+ def __post_init__(self) -> None:
17
+ scope_type = str(self.scope_type).strip()
18
+ scope_value = str(self.scope_value).strip()
19
+ if not scope_type:
20
+ raise ValueError('scope_type cannot be empty.')
21
+ if not scope_value:
22
+ raise ValueError('scope_value cannot be empty.')
23
+ object.__setattr__(self, 'scope_type', scope_type)
24
+ object.__setattr__(self, 'scope_value', scope_value)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class BlockEntry:
29
+ scope: BlockScope
30
+ reason: str = ''
31
+ rule_name: str = ''
32
+ status_code: int = 429
33
+ message: str = DEFAULT_BLOCK_MESSAGE
34
+ created_at: datetime | None = None
35
+ expires_at: datetime | None = None
36
+ revoked_at: datetime | None = None
37
+ metadata: Mapping[str, object] | None = field(default=None)
38
+
39
+ def __post_init__(self) -> None:
40
+ object.__setattr__(
41
+ self, 'metadata', MappingProxyType(dict(self.metadata or {}))
42
+ )
43
+
44
+
45
+ class BlockStore(Protocol):
46
+ def block(
47
+ self,
48
+ scope: BlockScope,
49
+ *,
50
+ reason: str = '',
51
+ rule_name: str = '',
52
+ status_code: int = 429,
53
+ message: str = DEFAULT_BLOCK_MESSAGE,
54
+ ttl: int | None = None,
55
+ metadata: Mapping[str, object] | None = None,
56
+ ) -> BlockEntry: ...
57
+
58
+ def unblock(self, scope: BlockScope, *, reason: str = '') -> int: ...
59
+
60
+ def get_active(self, scope: BlockScope) -> BlockEntry | None: ...
61
+
62
+ def first_active(self, scopes: Sequence[BlockScope]) -> BlockEntry | None: ...