sec-audit-rules 0.1.0a2__py3-none-any.whl

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.
@@ -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: ...
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Mapping
5
+
6
+ from sec_audit.core.config_validation import int_value, str_value
7
+ from sec_audit.core.exceptions import AuditConfigurationError
8
+
9
+ RULE_ACTIONS = {'observe', 'alert', 'block', 'temp_block', 'persist_block'}
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class EnforcementAuditConfig:
14
+ enforcement_block_severity: int | None = None
15
+ enforcement_status_code: int = 429
16
+ enforcement_message: str = 'Request blocked by audit enforcement policy'
17
+ block_cache_ttl: int = 300
18
+ block_rules: dict[str, int] = field(default_factory=dict)
19
+ rule_actions: dict[str, object] = field(default_factory=dict)
20
+
21
+ def __post_init__(self) -> None:
22
+ if self.enforcement_block_severity is not None:
23
+ severity = int_value(
24
+ 'enforcement_block_severity', self.enforcement_block_severity
25
+ )
26
+ if severity < 0 or severity > 10:
27
+ raise AuditConfigurationError(
28
+ 'enforcement_block_severity must be None or between 0 and 10.'
29
+ )
30
+ object.__setattr__(self, 'enforcement_block_severity', severity)
31
+ status_code = int_value('enforcement_status_code', self.enforcement_status_code)
32
+ if status_code < 100 or status_code > 599:
33
+ raise AuditConfigurationError(
34
+ 'enforcement_status_code must be an HTTP status code.'
35
+ )
36
+ object.__setattr__(self, 'enforcement_status_code', status_code)
37
+ object.__setattr__(
38
+ self,
39
+ 'enforcement_message',
40
+ str_value('enforcement_message', self.enforcement_message),
41
+ )
42
+ block_cache_ttl = int_value('block_cache_ttl', self.block_cache_ttl)
43
+ if block_cache_ttl < 0:
44
+ raise AuditConfigurationError(
45
+ 'block_cache_ttl must be greater than or equal to 0.'
46
+ )
47
+ object.__setattr__(self, 'block_cache_ttl', block_cache_ttl)
48
+ object.__setattr__(self, 'block_rules', _validate_block_rules(self.block_rules))
49
+ object.__setattr__(
50
+ self, 'rule_actions', _validate_rule_actions(self.rule_actions)
51
+ )
52
+
53
+
54
+ def _validate_block_rules(value: object) -> dict[str, int]:
55
+ if not isinstance(value, Mapping):
56
+ raise AuditConfigurationError('block_rules must be a mapping.')
57
+ parsed = {}
58
+ for rule_name, ttl in value.items():
59
+ parsed[str(rule_name)] = int_value('block_rules TTL', ttl)
60
+ if parsed[str(rule_name)] <= 0:
61
+ raise AuditConfigurationError('block_rules TTLs must be positive.')
62
+ return parsed
63
+
64
+
65
+ def _validate_rule_actions(value: object) -> dict[str, object]:
66
+ if not isinstance(value, Mapping):
67
+ raise AuditConfigurationError('rule_actions must be a mapping.')
68
+ parsed = {}
69
+ for rule_name, spec in value.items():
70
+ if isinstance(spec, str):
71
+ normalized = {'action': spec}
72
+ elif isinstance(spec, Mapping):
73
+ normalized = dict(spec)
74
+ else:
75
+ raise AuditConfigurationError(
76
+ 'rule_actions values must be action strings or mappings.'
77
+ )
78
+ action = normalized.get('action')
79
+ if not isinstance(action, str) or action not in RULE_ACTIONS:
80
+ raise AuditConfigurationError(
81
+ f'rule_actions action for {rule_name!r} must be one of: '
82
+ f'{", ".join(sorted(RULE_ACTIONS))}.'
83
+ )
84
+ if 'ttl' in normalized and normalized['ttl'] is not None:
85
+ normalized['ttl'] = int_value('rule_actions ttl', normalized['ttl'])
86
+ if normalized['ttl'] <= 0:
87
+ raise AuditConfigurationError('rule_actions ttl must be positive.')
88
+ if 'status_code' in normalized and normalized['status_code'] is not None:
89
+ normalized['status_code'] = int_value(
90
+ 'rule_actions status_code', normalized['status_code']
91
+ )
92
+ if normalized['status_code'] < 100 or normalized['status_code'] > 599:
93
+ raise AuditConfigurationError(
94
+ 'rule_actions status_code must be an HTTP status code.'
95
+ )
96
+ if 'scopes' in normalized and isinstance(normalized['scopes'], str):
97
+ raise AuditConfigurationError('rule_actions scopes must be a sequence.')
98
+ parsed[str(rule_name)] = normalized
99
+ return parsed
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Mapping, Protocol, Sequence
5
+
6
+ from sec_audit.rules.base import RuleMatch
7
+ from sec_audit.rules.events import RuleEvent
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class EnforcementDecision:
12
+ allowed: bool = True
13
+ status_code: int = 429
14
+ message: str = 'Request blocked by audit enforcement policy'
15
+ reason: str | None = None
16
+
17
+
18
+ class EnforcementPolicy(Protocol):
19
+ def decide(
20
+ self,
21
+ event: RuleEvent | Mapping[str, object],
22
+ matches: Sequence[RuleMatch],
23
+ ) -> EnforcementDecision: ...
24
+
25
+
26
+ def highest_severity_match(matches: Sequence[RuleMatch]) -> RuleMatch | None:
27
+ best = None
28
+ for match in matches:
29
+ if best is None or match.severity > best.severity:
30
+ best = match
31
+ return best
32
+
33
+
34
+ class SeverityEnforcementPolicy:
35
+ def __init__(
36
+ self,
37
+ *,
38
+ block_severity: int | None = 8,
39
+ status_code: int = 429,
40
+ message: str = 'Request blocked by audit enforcement policy',
41
+ ) -> None:
42
+ self.block_severity = (
43
+ int(block_severity) if block_severity is not None else None
44
+ )
45
+ self.status_code = int(status_code)
46
+ self.message = message
47
+
48
+ def decide(
49
+ self,
50
+ event: RuleEvent | Mapping[str, object],
51
+ matches: Sequence[RuleMatch],
52
+ ) -> EnforcementDecision:
53
+ if self.block_severity is None:
54
+ return EnforcementDecision()
55
+ triggering = highest_severity_match(
56
+ [match for match in matches if match.severity >= self.block_severity]
57
+ )
58
+ if triggering is None:
59
+ return EnforcementDecision()
60
+ return EnforcementDecision(
61
+ allowed=False,
62
+ status_code=self.status_code,
63
+ message=self.message,
64
+ reason=triggering.rule_name,
65
+ )
@@ -0,0 +1,3 @@
1
+ from sec_audit.integrations.wazuh.api import WazuhAPISource
2
+
3
+ __all__ = ['WazuhAPISource']
@@ -0,0 +1,69 @@
1
+ import time
2
+
3
+
4
+ class WazuhAPISource:
5
+ def __init__(
6
+ self,
7
+ *,
8
+ base_url: str = 'https://localhost:55000',
9
+ username: str = 'admin',
10
+ password: str = 'admin',
11
+ verify_ssl: bool = False,
12
+ ) -> None:
13
+ import httpx
14
+
15
+ self.base_url = base_url.rstrip('/')
16
+ self.username = username
17
+ self.password = password
18
+ self._session = httpx.Client(verify=verify_ssl)
19
+ self._token = None
20
+ self._token_expiry = 0.0
21
+
22
+ def _authenticate(self) -> bool:
23
+ now = time.time()
24
+ if self._token and now < self._token_expiry - 30:
25
+ return True
26
+ try:
27
+ resp = self._session.post(
28
+ f'{self.base_url}/security/user/authenticate',
29
+ json={},
30
+ auth=(self.username, self.password),
31
+ timeout=3,
32
+ )
33
+ resp.raise_for_status()
34
+ self._token = resp.json()['data']['token']
35
+ self._token_expiry = now + 3600
36
+ return True
37
+ except Exception:
38
+ return False
39
+
40
+ def is_available(self) -> bool:
41
+ return self._authenticate()
42
+
43
+ def request(self, method: str, path: str, **kwargs):
44
+ if not self._authenticate():
45
+ return None
46
+ headers = kwargs.pop('headers', {})
47
+ headers['Authorization'] = f'Bearer {self._token}'
48
+ try:
49
+ resp = self._session.request(
50
+ method,
51
+ f'{self.base_url}{path}',
52
+ headers=headers,
53
+ timeout=kwargs.pop('timeout', 15),
54
+ **kwargs,
55
+ )
56
+ resp.raise_for_status()
57
+ return resp.json()
58
+ except Exception:
59
+ return None
60
+
61
+ def get_agents(self):
62
+ result = self.request(
63
+ 'GET', '/agents', params={'select': 'id,name,status,lastKeepAlive'}
64
+ )
65
+ return (result or {}).get('data', {})
66
+
67
+ def get_manager_info(self):
68
+ result = self.request('GET', '/manager/info')
69
+ return (result or {}).get('data', {})
@@ -0,0 +1,7 @@
1
+ <group name="sec_audit,">
2
+ <rule id="100080" level="3">
3
+ <decoded_as>json</decoded_as>
4
+ <field name="attributes.schema_version">1.0</field>
5
+ <description>sec_audit event</description>
6
+ </rule>
7
+ </group>
@@ -0,0 +1,8 @@
1
+ title: sec_audit rule match
2
+ logsource:
3
+ product: python
4
+ detection:
5
+ selection:
6
+ attributes.event_type: audit.rule.match
7
+ condition: selection
8
+ level: medium
@@ -0,0 +1,50 @@
1
+ from sec_audit.rules.base import (
2
+ ContextRequirements,
3
+ Rule,
4
+ RuleContext,
5
+ RuleMatch,
6
+ ScopeContext,
7
+ ScopedHistoryReader,
8
+ )
9
+ from sec_audit.rules.builtins import (
10
+ BruteForceLoginRule,
11
+ LoginThrottleRule,
12
+ RepeatedClientErrorRule,
13
+ RepeatedRouteRule,
14
+ RequestBodyThresholdRule,
15
+ SensitiveFieldChangeRule,
16
+ SuspiciousProxyHeaderRule,
17
+ )
18
+ from sec_audit.rules.config import RulesAuditConfig
19
+ from sec_audit.rules.engine import RuleEngine
20
+ from sec_audit.rules.events import RuleEvent, SummaryKey
21
+ from sec_audit.rules.history import (
22
+ HistoryScopeExtractor,
23
+ ScopeKey,
24
+ build_history_scope_extractors,
25
+ extract_scope_keys,
26
+ )
27
+
28
+ __all__ = [
29
+ 'BruteForceLoginRule',
30
+ 'ContextRequirements',
31
+ 'LoginThrottleRule',
32
+ 'RepeatedClientErrorRule',
33
+ 'RepeatedRouteRule',
34
+ 'RequestBodyThresholdRule',
35
+ 'Rule',
36
+ 'RuleContext',
37
+ 'RuleEngine',
38
+ 'RuleEvent',
39
+ 'RuleMatch',
40
+ 'RulesAuditConfig',
41
+ 'ScopeContext',
42
+ 'ScopeKey',
43
+ 'ScopedHistoryReader',
44
+ 'SensitiveFieldChangeRule',
45
+ 'SuspiciousProxyHeaderRule',
46
+ 'SummaryKey',
47
+ 'HistoryScopeExtractor',
48
+ 'build_history_scope_extractors',
49
+ 'extract_scope_keys',
50
+ ]