omnibase_infra 0.2.1__py3-none-any.whl → 0.2.3__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.
Files changed (161) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
  3. omnibase_infra/capabilities/__init__.py +15 -0
  4. omnibase_infra/capabilities/capability_inference_rules.py +211 -0
  5. omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
  6. omnibase_infra/capabilities/intent_type_extractor.py +160 -0
  7. omnibase_infra/cli/commands.py +1 -1
  8. omnibase_infra/configs/widget_mapping.yaml +176 -0
  9. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +5 -2
  10. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +5 -2
  11. omnibase_infra/enums/__init__.py +6 -0
  12. omnibase_infra/enums/enum_handler_error_type.py +10 -0
  13. omnibase_infra/enums/enum_handler_source_mode.py +72 -0
  14. omnibase_infra/enums/enum_kafka_acks.py +99 -0
  15. omnibase_infra/errors/error_compute_registry.py +4 -1
  16. omnibase_infra/errors/error_event_bus_registry.py +4 -1
  17. omnibase_infra/errors/error_infra.py +3 -1
  18. omnibase_infra/errors/error_policy_registry.py +4 -1
  19. omnibase_infra/event_bus/event_bus_kafka.py +1 -1
  20. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +59 -10
  21. omnibase_infra/handlers/__init__.py +8 -1
  22. omnibase_infra/handlers/handler_consul.py +7 -1
  23. omnibase_infra/handlers/handler_db.py +10 -3
  24. omnibase_infra/handlers/handler_graph.py +10 -5
  25. omnibase_infra/handlers/handler_http.py +8 -2
  26. omnibase_infra/handlers/handler_intent.py +387 -0
  27. omnibase_infra/handlers/handler_mcp.py +745 -63
  28. omnibase_infra/handlers/handler_vault.py +11 -5
  29. omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
  30. omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
  31. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +7 -0
  32. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +308 -4
  33. omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
  34. omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
  35. omnibase_infra/mixins/mixin_node_introspection.py +42 -7
  36. omnibase_infra/mixins/mixin_retry_execution.py +1 -1
  37. omnibase_infra/models/discovery/model_introspection_config.py +11 -0
  38. omnibase_infra/models/handlers/__init__.py +48 -5
  39. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  40. omnibase_infra/models/handlers/model_contract_discovery_result.py +6 -4
  41. omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
  42. omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
  43. omnibase_infra/models/mcp/__init__.py +15 -0
  44. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  45. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  46. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  47. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  48. omnibase_infra/models/registration/model_node_capabilities.py +11 -0
  49. omnibase_infra/models/registration/model_node_introspection_event.py +9 -0
  50. omnibase_infra/models/runtime/model_handler_contract.py +25 -9
  51. omnibase_infra/models/runtime/model_loaded_handler.py +9 -0
  52. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
  53. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
  54. omnibase_infra/nodes/effects/contract.yaml +0 -5
  55. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
  56. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
  57. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
  58. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +1 -1
  59. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
  60. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +4 -3
  61. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
  62. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
  63. omnibase_infra/nodes/node_registration_storage_effect/node.py +4 -1
  64. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +47 -26
  65. omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
  66. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
  67. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +28 -20
  68. omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
  69. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
  70. omnibase_infra/plugins/plugin_compute_base.py +16 -2
  71. omnibase_infra/protocols/__init__.py +2 -0
  72. omnibase_infra/protocols/protocol_container_aware.py +200 -0
  73. omnibase_infra/protocols/protocol_event_projector.py +1 -1
  74. omnibase_infra/runtime/__init__.py +90 -1
  75. omnibase_infra/runtime/binding_config_resolver.py +102 -37
  76. omnibase_infra/runtime/constants_notification.py +75 -0
  77. omnibase_infra/runtime/contract_handler_discovery.py +6 -1
  78. omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
  79. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  80. omnibase_infra/runtime/handler_contract_source.py +267 -186
  81. omnibase_infra/runtime/handler_identity.py +81 -0
  82. omnibase_infra/runtime/handler_plugin_loader.py +19 -2
  83. omnibase_infra/runtime/handler_registry.py +11 -3
  84. omnibase_infra/runtime/handler_source_resolver.py +326 -0
  85. omnibase_infra/runtime/mixin_semver_cache.py +25 -1
  86. omnibase_infra/runtime/mixins/__init__.py +7 -0
  87. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  88. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
  89. omnibase_infra/runtime/models/__init__.py +24 -0
  90. omnibase_infra/runtime/models/model_health_check_result.py +2 -1
  91. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  92. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  93. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  94. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  95. omnibase_infra/runtime/projector_plugin_loader.py +1 -1
  96. omnibase_infra/runtime/projector_shell.py +229 -1
  97. omnibase_infra/runtime/protocol_lifecycle_executor.py +6 -6
  98. omnibase_infra/runtime/protocols/__init__.py +10 -0
  99. omnibase_infra/runtime/registry/registry_protocol_binding.py +16 -15
  100. omnibase_infra/runtime/registry_contract_source.py +693 -0
  101. omnibase_infra/runtime/registry_policy.py +9 -326
  102. omnibase_infra/runtime/secret_resolver.py +4 -2
  103. omnibase_infra/runtime/service_kernel.py +11 -3
  104. omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
  105. omnibase_infra/runtime/service_runtime_host_process.py +589 -106
  106. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  107. omnibase_infra/runtime/transition_notification_publisher.py +764 -0
  108. omnibase_infra/runtime/util_container_wiring.py +6 -5
  109. omnibase_infra/runtime/util_wiring.py +17 -4
  110. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  111. omnibase_infra/services/__init__.py +21 -0
  112. omnibase_infra/services/corpus_capture.py +7 -1
  113. omnibase_infra/services/mcp/__init__.py +31 -0
  114. omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
  115. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  116. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  117. omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
  118. omnibase_infra/services/registry_api/__init__.py +40 -0
  119. omnibase_infra/services/registry_api/main.py +261 -0
  120. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  121. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  122. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  123. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  124. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  125. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  126. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  127. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  128. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  129. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  130. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  131. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  132. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  133. omnibase_infra/services/registry_api/routes.py +371 -0
  134. omnibase_infra/services/registry_api/service.py +837 -0
  135. omnibase_infra/services/service_capability_query.py +4 -4
  136. omnibase_infra/services/service_health.py +3 -2
  137. omnibase_infra/services/service_timeout_emitter.py +20 -3
  138. omnibase_infra/services/service_timeout_scanner.py +7 -3
  139. omnibase_infra/services/session/__init__.py +56 -0
  140. omnibase_infra/services/session/config_consumer.py +120 -0
  141. omnibase_infra/services/session/config_store.py +139 -0
  142. omnibase_infra/services/session/consumer.py +1007 -0
  143. omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
  144. omnibase_infra/services/session/store.py +997 -0
  145. omnibase_infra/utils/__init__.py +19 -0
  146. omnibase_infra/utils/util_atomic_file.py +261 -0
  147. omnibase_infra/utils/util_db_transaction.py +239 -0
  148. omnibase_infra/utils/util_dsn_validation.py +1 -1
  149. omnibase_infra/utils/util_retry_optimistic.py +281 -0
  150. omnibase_infra/validation/__init__.py +3 -19
  151. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  152. omnibase_infra/validation/infra_validators.py +35 -24
  153. omnibase_infra/validation/validation_exemptions.yaml +140 -9
  154. omnibase_infra/validation/validator_chain_propagation.py +2 -2
  155. omnibase_infra/validation/validator_runtime_shape.py +1 -1
  156. omnibase_infra/validation/validator_security.py +473 -370
  157. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/METADATA +3 -3
  158. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/RECORD +161 -98
  159. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/WHEEL +0 -0
  160. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/entry_points.txt +0 -0
  161. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,410 +1,513 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  # Copyright (c) 2025 OmniNode Team
3
- """Security Validation for ONEX Infrastructure.
3
+ """Security Validator for ONEX Infrastructure.
4
4
 
5
- This module provides security validation utilities for handler introspection,
6
- method exposure, and security constraint enforcement. Part of OMN-1091:
7
- Structured Validation & Error Reporting for Handlers.
5
+ Contract-driven AST validator for detecting security concerns in Python code.
6
+ Part of OMN-1277: Refactor validators to be Handler and contract-driven.
8
7
 
9
8
  Security Validation Scope:
10
- - Method exposure via introspection (sensitive method names)
11
- - Credential leakage in public method signatures
12
- - Insecure patterns in handler code
13
- - Missing authentication/authorization decorators
14
-
15
- Integration Status:
16
- This module is a PLACEHOLDER for future security validation integration.
17
- As of implementation, the security validation logic needs to be integrated
18
- with the following components:
19
-
20
- 1. MixinNodeIntrospection._should_skip_method() - Add validation
21
- 2. Contract linting - Add security rule checks
22
- 3. Static analysis - Add AST-based security scanning
23
- 4. Runtime validation - Add security constraint enforcement
9
+ - Public methods with sensitive names (get_password, get_secret, etc.)
10
+ - Method signatures containing sensitive parameter names
11
+ - Admin/internal methods exposed without underscore prefix
12
+ - Decrypt operations exposed publicly
24
13
 
25
14
  Usage:
26
- >>> from omnibase_infra.validation.validator_security import (
27
- ... validate_method_exposure,
28
- ... validate_handler_security,
29
- ... )
15
+ >>> from pathlib import Path
16
+ >>> from omnibase_infra.validation.validator_security import ValidatorSecurity
30
17
  >>>
31
- >>> # Validate method names for security concerns
32
- >>> errors = validate_method_exposure(
33
- ... method_names=["get_credentials", "process_request"],
34
- ... handler_identity=ModelHandlerIdentifier.from_handler_id("auth-handler"),
35
- ... )
36
- >>>
37
- >>> # Check for security violations
38
- >>> for error in errors:
39
- ... print(error.format_for_logging())
18
+ >>> validator = ValidatorSecurity()
19
+ >>> result = validator.validate(Path("src/"))
20
+ >>> if not result.is_valid:
21
+ ... for issue in result.issues:
22
+ ... print(f"{issue.file_path}:{issue.line_number}: {issue.message}")
23
+
24
+ CLI Usage:
25
+ python -m omnibase_infra.validation.validator_security src/
40
26
 
41
27
  See Also:
42
28
  - docs/patterns/security_patterns.md - Comprehensive security guide
43
- - MixinNodeIntrospection - Node capability discovery with security filtering
44
- - ModelHandlerValidationError - Structured error reporting
45
- - EnumHandlerErrorType.SECURITY_VALIDATION_ERROR - Error classification
29
+ - ValidatorBase - Base class for contract-driven validators
46
30
  """
47
31
 
48
32
  from __future__ import annotations
49
33
 
34
+ import ast
35
+ import logging
50
36
  import re
51
- from typing import Final
52
-
53
- from omnibase_infra.models.errors import ModelHandlerValidationError
54
- from omnibase_infra.models.handlers import ModelHandlerIdentifier
55
- from omnibase_infra.models.validation import ModelValidationErrorParams
56
-
57
- # Security rule IDs for structured error reporting
58
- # These IDs enable tracking and remediation of specific security violations
59
-
60
-
61
- class SecurityRuleId:
62
- """Rule IDs for security validation errors.
63
-
64
- These IDs provide unique identifiers for each type of security validation
65
- failure, enabling structured error tracking and remediation guidance.
66
- """
67
-
68
- # Method exposure violations (SECURITY-001 to SECURITY-099)
69
- SENSITIVE_METHOD_EXPOSED = "SECURITY-001"
70
- CREDENTIAL_IN_SIGNATURE = "SECURITY-002"
71
- ADMIN_METHOD_PUBLIC = "SECURITY-003"
72
- DECRYPT_METHOD_PUBLIC = "SECURITY-004"
73
-
74
- # Configuration violations (SECURITY-100 to SECURITY-199)
75
- CREDENTIAL_IN_CONFIG = "SECURITY-100"
76
- HARDCODED_SECRET = "SECURITY-101"
77
- INSECURE_CONNECTION = "SECURITY-102"
78
-
79
- # Pattern violations (SECURITY-200 to SECURITY-299)
80
- INSECURE_PATTERN = "SECURITY-200"
81
- MISSING_AUTH_CHECK = "SECURITY-201"
82
- MISSING_INPUT_VALIDATION = "SECURITY-202"
83
-
84
-
85
- # Sensitive method name patterns that should be prefixed with underscore
86
- # These patterns indicate methods that expose sensitive operations
87
- # Pre-compiled for performance
88
- _SENSITIVE_METHOD_PATTERN_STRINGS: Final[tuple[str, ...]] = (
89
- r"^get_password$",
90
- r"^get_secret$",
91
- r"^get_token$",
92
- r"^get_api_key$",
93
- r"^get_credential",
94
- r"^fetch_password$",
95
- r"^fetch_secret$",
96
- r"^fetch_token$",
97
- r"^decrypt_",
98
- r"^admin_",
99
- r"^internal_",
100
- r"^validate_password$",
101
- r"^check_password$",
102
- r"^verify_password$",
103
- )
104
-
105
- # Pre-compiled regex patterns for efficient matching
106
- SENSITIVE_METHOD_PATTERNS: Final[tuple[re.Pattern[str], ...]] = tuple(
107
- re.compile(pattern) for pattern in _SENSITIVE_METHOD_PATTERN_STRINGS
108
- )
109
-
110
- # Patterns that should map to ADMIN_METHOD_PUBLIC rule (SECURITY-003)
111
- _ADMIN_INTERNAL_PATTERN_STRINGS: Final[tuple[str, ...]] = (
112
- r"^admin_",
113
- r"^internal_",
114
- )
115
-
116
- # Pre-compiled admin/internal patterns
117
- _ADMIN_INTERNAL_PATTERNS: Final[tuple[re.Pattern[str], ...]] = tuple(
118
- re.compile(pattern) for pattern in _ADMIN_INTERNAL_PATTERN_STRINGS
119
- )
120
-
121
- # Parameter names that indicate sensitive data in method signatures
122
- SENSITIVE_PARAMETER_NAMES: Final[frozenset[str]] = frozenset(
123
- {
124
- "password",
125
- "secret",
126
- "token",
127
- "api_key",
128
- "apikey",
129
- "access_key",
130
- "private_key",
131
- "credential",
132
- "auth_token",
133
- "bearer_token",
134
- "decrypt_key",
135
- "encryption_key",
136
- }
37
+ import sys
38
+ from dataclasses import dataclass, field
39
+ from pathlib import Path
40
+ from typing import ClassVar
41
+
42
+ from omnibase_core.models.common.model_validation_issue import ModelValidationIssue
43
+ from omnibase_core.models.contracts.subcontracts.model_validator_subcontract import (
44
+ ModelValidatorSubcontract,
137
45
  )
46
+ from omnibase_core.validation.validator_base import ValidatorBase
138
47
 
48
+ # Configure logger for this module
49
+ logger = logging.getLogger(__name__)
139
50
 
140
- def is_sensitive_method_name(method_name: str) -> bool:
141
- """Check if method name matches sensitive operation patterns.
142
51
 
143
- Args:
144
- method_name: Name of the method to check
52
+ @dataclass(frozen=True, slots=True)
53
+ class CompiledPatterns:
54
+ """Pre-compiled regex patterns for security validation.
145
55
 
146
- Returns:
147
- True if method name indicates a sensitive operation
56
+ Patterns are compiled once per contract/file validation, not per method.
57
+ This significantly improves performance for large codebases by avoiding
58
+ repeated pattern extraction from contract rules and regex compilation.
148
59
 
149
- Example:
150
- >>> is_sensitive_method_name("get_password")
151
- True
152
- >>> is_sensitive_method_name("process_request")
153
- False
60
+ Attributes:
61
+ admin_patterns: Compiled patterns for admin_method_public rule.
62
+ decrypt_patterns: Compiled patterns for decrypt_method_public rule.
63
+ sensitive_patterns: Compiled patterns for sensitive_method_exposed rule.
64
+ sensitive_params: Lowercase sensitive parameter names (exact match, no regex).
154
65
  """
155
- method_lower = method_name.lower()
156
66
 
157
- for pattern in SENSITIVE_METHOD_PATTERNS:
158
- if pattern.match(method_lower):
159
- return True
67
+ admin_patterns: tuple[re.Pattern[str], ...] = field(default_factory=tuple)
68
+ decrypt_patterns: tuple[re.Pattern[str], ...] = field(default_factory=tuple)
69
+ sensitive_patterns: tuple[re.Pattern[str], ...] = field(default_factory=tuple)
70
+ sensitive_params: frozenset[str] = field(default_factory=frozenset)
160
71
 
161
- return False
162
72
 
73
+ @dataclass(frozen=True, slots=True)
74
+ class MethodContext:
75
+ """Context for method validation - groups related parameters."""
163
76
 
164
- def _get_sensitivity_rule_id(method_name: str) -> str | None:
165
- """Determine the appropriate security rule ID for a sensitive method.
77
+ method_name: str
78
+ class_name: str
79
+ file_path: Path
80
+ line_number: int
81
+ contract: ModelValidatorSubcontract
82
+ compiled_patterns: CompiledPatterns
166
83
 
167
- Args:
168
- method_name: Name of the method to check
169
84
 
170
- Returns:
171
- Security rule ID if method is sensitive, None otherwise
85
+ class ValidatorSecurity(ValidatorBase):
86
+ """Contract-driven security validator for Python source files.
172
87
 
173
- Note:
174
- - admin_/internal_ methods map to SECURITY-003 (ADMIN_METHOD_PUBLIC)
175
- - Other sensitive patterns map to SECURITY-001 (SENSITIVE_METHOD_EXPOSED)
88
+ This validator uses AST analysis to detect security concerns in Python code:
89
+ - Public methods with sensitive names (get_password, get_secret, etc.)
90
+ - Method signatures containing sensitive parameter names
91
+ - Admin/internal methods exposed without underscore prefix
92
+ - Decrypt operations exposed publicly
93
+
94
+ The validator is contract-driven via security.validation.yaml, supporting:
95
+ - Configurable rules with enable/disable per rule
96
+ - Per-rule severity overrides
97
+ - Suppression comments for intentional exceptions
98
+ - Glob-based file targeting and exclusion
99
+
100
+ Thread Safety:
101
+ ValidatorSecurity instances are NOT thread-safe due to internal mutable
102
+ state inherited from ValidatorBase. When using parallel execution
103
+ (e.g., pytest-xdist), create separate validator instances per worker.
104
+
105
+ Attributes:
106
+ validator_id: Unique identifier for this validator ("security").
107
+
108
+ Usage Example:
109
+ >>> from pathlib import Path
110
+ >>> from omnibase_infra.validation.validator_security import ValidatorSecurity
111
+ >>> validator = ValidatorSecurity()
112
+ >>> result = validator.validate(Path("src/"))
113
+ >>> print(f"Valid: {result.is_valid}, Issues: {len(result.issues)}")
114
+
115
+ CLI Usage:
116
+ python -m omnibase_infra.validation.validator_security src/
176
117
  """
177
- method_lower = method_name.lower()
178
-
179
- # Check admin/internal patterns first (SECURITY-003)
180
- for pattern in _ADMIN_INTERNAL_PATTERNS:
181
- if pattern.match(method_lower):
182
- return SecurityRuleId.ADMIN_METHOD_PUBLIC
183
-
184
- # Check other sensitive patterns (SECURITY-001)
185
- for pattern in SENSITIVE_METHOD_PATTERNS:
186
- if pattern.match(method_lower):
187
- return SecurityRuleId.SENSITIVE_METHOD_EXPOSED
188
-
189
- return None
190
118
 
119
+ # ONEX_EXCLUDE: string_id - human-readable validator identifier
120
+ validator_id: ClassVar[str] = "security"
121
+
122
+ def _get_rule_patterns(
123
+ self,
124
+ rule_id: str,
125
+ param_key: str,
126
+ contract: ModelValidatorSubcontract,
127
+ ) -> list[str]:
128
+ """Extract patterns from a rule's parameters in the contract.
129
+
130
+ Searches the contract's rules for the specified rule_id and extracts
131
+ the list of patterns from the specified parameter key.
132
+
133
+ Args:
134
+ rule_id: The rule identifier to look up (e.g., "sensitive_method_exposed").
135
+ param_key: The parameter key containing the patterns (e.g., "patterns").
136
+ contract: Validator contract with rule configurations.
137
+
138
+ Returns:
139
+ List of pattern strings from the rule's parameters. Returns an empty
140
+ list if the rule is not found, has no parameters, or the param_key
141
+ is missing/invalid (fail-safe behavior).
142
+ """
143
+ for rule in contract.rules:
144
+ if rule.rule_id == rule_id:
145
+ if rule.parameters is None:
146
+ logger.debug(
147
+ "Rule %s has no parameters, returning empty list",
148
+ rule_id,
149
+ )
150
+ return []
151
+ patterns = rule.parameters.get(param_key)
152
+ if patterns is None:
153
+ logger.debug(
154
+ "Rule %s missing param_key %s, returning empty list",
155
+ rule_id,
156
+ param_key,
157
+ )
158
+ return []
159
+ if isinstance(patterns, list):
160
+ # Ensure all items are strings
161
+ return [str(p) for p in patterns]
162
+ logger.warning(
163
+ "Rule %s param_key %s is not a list: %s",
164
+ rule_id,
165
+ param_key,
166
+ type(patterns).__name__,
167
+ )
168
+ return []
169
+ logger.debug("Rule %s not found in contract, returning empty list", rule_id)
170
+ return []
171
+
172
+ def _compile_patterns(
173
+ self,
174
+ contract: ModelValidatorSubcontract,
175
+ ) -> CompiledPatterns:
176
+ """Compile and cache all regex patterns from contract rules.
177
+
178
+ This method extracts patterns from the contract once per file validation,
179
+ compiles them into re.Pattern objects, and returns a frozen dataclass.
180
+ This avoids repeated pattern extraction and compilation for every method.
181
+
182
+ Args:
183
+ contract: Validator contract with rule configurations.
184
+
185
+ Returns:
186
+ CompiledPatterns instance with pre-compiled patterns.
187
+ """
188
+
189
+ def compile_pattern_list(patterns: list[str]) -> tuple[re.Pattern[str], ...]:
190
+ """Compile a list of pattern strings into regex Pattern objects."""
191
+ compiled: list[re.Pattern[str]] = []
192
+ for pattern in patterns:
193
+ try:
194
+ compiled.append(re.compile(pattern))
195
+ except re.error as e:
196
+ logger.warning(
197
+ "Invalid regex pattern '%s': %s - skipping",
198
+ pattern,
199
+ e,
200
+ )
201
+ return tuple(compiled)
202
+
203
+ # Extract and compile patterns for each rule
204
+ admin_patterns = compile_pattern_list(
205
+ self._get_rule_patterns("admin_method_public", "patterns", contract)
206
+ )
207
+ decrypt_patterns = compile_pattern_list(
208
+ self._get_rule_patterns("decrypt_method_public", "patterns", contract)
209
+ )
210
+ sensitive_patterns = compile_pattern_list(
211
+ self._get_rule_patterns("sensitive_method_exposed", "patterns", contract)
212
+ )
213
+
214
+ # Extract sensitive parameter names (exact match, no regex compilation needed)
215
+ sensitive_params_list = self._get_rule_patterns(
216
+ "credential_in_signature", "sensitive_params", contract
217
+ )
218
+ sensitive_params = frozenset(p.lower() for p in sensitive_params_list)
219
+
220
+ return CompiledPatterns(
221
+ admin_patterns=admin_patterns,
222
+ decrypt_patterns=decrypt_patterns,
223
+ sensitive_patterns=sensitive_patterns,
224
+ sensitive_params=sensitive_params,
225
+ )
226
+
227
+ def _validate_file(
228
+ self,
229
+ path: Path,
230
+ contract: ModelValidatorSubcontract,
231
+ ) -> tuple[ModelValidationIssue, ...]:
232
+ """Validate a single Python file for security violations.
233
+
234
+ Uses AST analysis to detect:
235
+ - Sensitive method names in class definitions
236
+ - Sensitive parameter names in method signatures
237
+
238
+ Args:
239
+ path: Path to the Python file to validate.
240
+ contract: Validator contract with configuration.
241
+
242
+ Returns:
243
+ Tuple of ModelValidationIssue instances for violations found.
244
+ """
245
+ try:
246
+ source = path.read_text(encoding="utf-8")
247
+ except OSError as e:
248
+ # fallback-ok: log warning and skip file on read errors
249
+ logger.warning("Cannot read file %s: %s", path, e)
250
+ return ()
251
+
252
+ try:
253
+ tree = ast.parse(source, filename=str(path))
254
+ except SyntaxError as e:
255
+ # fallback-ok: log warning and skip file with syntax errors
256
+ logger.warning(
257
+ "Skipping file with syntax error: path=%s, line=%s, error=%s",
258
+ path,
259
+ e.lineno,
260
+ e.msg,
261
+ )
262
+ return ()
191
263
 
192
- def has_sensitive_parameters(signature: str) -> list[str]:
193
- """Extract sensitive parameter names from method signature.
194
-
195
- Args:
196
- signature: Method signature string (e.g., "(username: str, password: str)")
264
+ issues: list[ModelValidationIssue] = []
197
265
 
198
- Returns:
199
- List of sensitive parameter names found in signature
266
+ # Compile patterns once for the entire file validation
267
+ # This avoids repeated pattern extraction and regex compilation per method
268
+ compiled_patterns = self._compile_patterns(contract)
200
269
 
201
- Example:
202
- >>> has_sensitive_parameters("(username: str, password: str)")
203
- ['password']
204
- >>> has_sensitive_parameters("(user_id: UUID, data: dict)")
205
- []
206
- """
207
- signature_lower = signature.lower()
208
- found_sensitive = []
270
+ # Visit all class definitions in the file
271
+ for node in ast.walk(tree):
272
+ if isinstance(node, ast.ClassDef):
273
+ class_issues = self._check_class_methods(
274
+ node, path, contract, compiled_patterns
275
+ )
276
+ issues.extend(class_issues)
277
+
278
+ return tuple(issues)
279
+
280
+ def _check_class_methods(
281
+ self,
282
+ class_node: ast.ClassDef,
283
+ file_path: Path,
284
+ contract: ModelValidatorSubcontract,
285
+ compiled_patterns: CompiledPatterns,
286
+ ) -> list[ModelValidationIssue]:
287
+ """Check class methods for security violations."""
288
+ issues: list[ModelValidationIssue] = []
289
+
290
+ for node in class_node.body:
291
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
292
+ method_name = node.name
293
+
294
+ # Skip private/protected methods (already safe)
295
+ if method_name.startswith("_"):
296
+ continue
297
+
298
+ # Skip dunder methods
299
+ if method_name.startswith("__") and method_name.endswith("__"):
300
+ continue
301
+
302
+ ctx = MethodContext(
303
+ method_name=method_name,
304
+ class_name=class_node.name,
305
+ file_path=file_path,
306
+ line_number=node.lineno,
307
+ contract=contract,
308
+ compiled_patterns=compiled_patterns,
309
+ )
310
+ method_issues = self._check_method(node, ctx)
311
+ issues.extend(method_issues)
312
+
313
+ return issues
314
+
315
+ def _check_method(
316
+ self,
317
+ method_node: ast.FunctionDef | ast.AsyncFunctionDef,
318
+ ctx: MethodContext,
319
+ ) -> list[ModelValidationIssue]:
320
+ """Check a single method for security violations."""
321
+ issues: list[ModelValidationIssue] = []
322
+
323
+ # Check for sensitive method names
324
+ method_issues = self._check_sensitive_method_name(ctx)
325
+ issues.extend(method_issues)
326
+
327
+ # Check for sensitive parameters in signature
328
+ param_issues = self._check_sensitive_parameters(method_node, ctx)
329
+ issues.extend(param_issues)
330
+
331
+ return issues
332
+
333
+ def _check_sensitive_method_name(
334
+ self,
335
+ ctx: MethodContext,
336
+ ) -> list[ModelValidationIssue]:
337
+ """Check if method name matches sensitive patterns.
338
+
339
+ Uses pre-compiled patterns from ctx.compiled_patterns for efficiency.
340
+ Patterns are originally read from contract.rules[].parameters.
341
+ """
342
+ issues: list[ModelValidationIssue] = []
343
+ method_lower = ctx.method_name.lower()
344
+
345
+ # Check admin/internal patterns (rule: admin_method_public)
346
+ # Uses pre-compiled patterns from ctx.compiled_patterns
347
+ for pattern in ctx.compiled_patterns.admin_patterns:
348
+ if pattern.match(method_lower):
349
+ enabled, severity = self._get_rule_config(
350
+ "admin_method_public", ctx.contract
351
+ )
352
+ if enabled:
353
+ issues.append(
354
+ ModelValidationIssue(
355
+ severity=severity,
356
+ message=f"Class '{ctx.class_name}' exposes admin/internal method '{ctx.method_name}'",
357
+ code="admin_method_public",
358
+ file_path=ctx.file_path,
359
+ line_number=ctx.line_number,
360
+ rule_name="admin_method_public",
361
+ suggestion=f"Prefix method with underscore: '_{ctx.method_name}' or move to separate admin module",
362
+ context={
363
+ "class_name": ctx.class_name,
364
+ "method_name": ctx.method_name,
365
+ "violation_type": "admin_method_public",
366
+ },
367
+ )
368
+ )
369
+ return issues # Don't double-report
370
+
371
+ # Check decrypt patterns (rule: decrypt_method_public)
372
+ # Uses pre-compiled patterns from ctx.compiled_patterns
373
+ for pattern in ctx.compiled_patterns.decrypt_patterns:
374
+ if pattern.match(method_lower):
375
+ enabled, severity = self._get_rule_config(
376
+ "decrypt_method_public", ctx.contract
377
+ )
378
+ if enabled:
379
+ issues.append(
380
+ ModelValidationIssue(
381
+ severity=severity,
382
+ message=f"Class '{ctx.class_name}' exposes decrypt method '{ctx.method_name}'",
383
+ code="decrypt_method_public",
384
+ file_path=ctx.file_path,
385
+ line_number=ctx.line_number,
386
+ rule_name="decrypt_method_public",
387
+ suggestion=f"Prefix method with underscore: '_{ctx.method_name}' to exclude from public API",
388
+ context={
389
+ "class_name": ctx.class_name,
390
+ "method_name": ctx.method_name,
391
+ "violation_type": "decrypt_method_public",
392
+ },
393
+ )
394
+ )
395
+ return issues # Don't double-report
396
+
397
+ # Check other sensitive patterns (rule: sensitive_method_exposed)
398
+ # Uses pre-compiled patterns from ctx.compiled_patterns
399
+ for pattern in ctx.compiled_patterns.sensitive_patterns:
400
+ if pattern.match(method_lower):
401
+ enabled, severity = self._get_rule_config(
402
+ "sensitive_method_exposed", ctx.contract
403
+ )
404
+ if enabled:
405
+ issues.append(
406
+ ModelValidationIssue(
407
+ severity=severity,
408
+ message=f"Class '{ctx.class_name}' exposes sensitive method '{ctx.method_name}'",
409
+ code="sensitive_method_exposed",
410
+ file_path=ctx.file_path,
411
+ line_number=ctx.line_number,
412
+ rule_name="sensitive_method_exposed",
413
+ suggestion=f"Prefix method with underscore: '_{ctx.method_name}' to exclude from introspection",
414
+ context={
415
+ "class_name": ctx.class_name,
416
+ "method_name": ctx.method_name,
417
+ "violation_type": "sensitive_method_exposed",
418
+ },
419
+ )
420
+ )
421
+ break
422
+
423
+ return issues
424
+
425
+ def _check_sensitive_parameters(
426
+ self,
427
+ method_node: ast.FunctionDef | ast.AsyncFunctionDef,
428
+ ctx: MethodContext,
429
+ ) -> list[ModelValidationIssue]:
430
+ """Check method signature for sensitive parameter names.
431
+
432
+ Uses pre-compiled sensitive_params frozenset from ctx.compiled_patterns.
433
+ Patterns are originally read from contract.rules[].parameters.
434
+ """
435
+ issues: list[ModelValidationIssue] = []
436
+
437
+ # Get rule configuration
438
+ enabled, severity = self._get_rule_config(
439
+ "credential_in_signature", ctx.contract
440
+ )
441
+ if not enabled:
442
+ return issues
443
+
444
+ # Use pre-compiled sensitive params (already lowercase frozenset)
445
+ sensitive_params = ctx.compiled_patterns.sensitive_params
446
+
447
+ # If no patterns defined in contract, return early (fail-safe)
448
+ if not sensitive_params:
449
+ return issues
450
+
451
+ # Extract parameter names from AST
452
+ found_sensitive: list[str] = []
453
+ for arg in method_node.args.args:
454
+ arg_name_lower = arg.arg.lower()
455
+ if arg_name_lower in sensitive_params:
456
+ found_sensitive.append(arg.arg)
457
+
458
+ # Also check keyword-only args (parameters after * in signature)
459
+ for arg in method_node.args.kwonlyargs:
460
+ arg_name_lower = arg.arg.lower()
461
+ if arg_name_lower in sensitive_params:
462
+ found_sensitive.append(arg.arg)
463
+
464
+ # Also check positional-only args (Python 3.8+, parameters before / in signature)
465
+ # Example: def authenticate(username, /, password): ...
466
+ # Here 'username' is positional-only and would be in posonlyargs
467
+ for arg in method_node.args.posonlyargs:
468
+ arg_name_lower = arg.arg.lower()
469
+ if arg_name_lower in sensitive_params:
470
+ found_sensitive.append(arg.arg)
471
+
472
+ # Check vararg (*args parameter)
473
+ # Example: def process(*passwords): ... would expose sensitive vararg name
474
+ if method_node.args.vararg:
475
+ vararg_name_lower = method_node.args.vararg.arg.lower()
476
+ if vararg_name_lower in sensitive_params:
477
+ found_sensitive.append(method_node.args.vararg.arg)
478
+
479
+ # Check kwarg (**kwargs parameter)
480
+ # Example: def process(**secrets): ... would expose sensitive kwarg name
481
+ if method_node.args.kwarg:
482
+ kwarg_name_lower = method_node.args.kwarg.arg.lower()
483
+ if kwarg_name_lower in sensitive_params:
484
+ found_sensitive.append(method_node.args.kwarg.arg)
485
+
486
+ if found_sensitive:
487
+ issues.append(
488
+ ModelValidationIssue(
489
+ severity=severity,
490
+ message=f"Method '{ctx.class_name}.{ctx.method_name}' has sensitive parameters: {', '.join(found_sensitive)}",
491
+ code="credential_in_signature",
492
+ file_path=ctx.file_path,
493
+ line_number=ctx.line_number,
494
+ rule_name="credential_in_signature",
495
+ suggestion=f"Use generic parameter names (e.g., 'data' instead of '{found_sensitive[0]}') or make method private",
496
+ context={
497
+ "class_name": ctx.class_name,
498
+ "method_name": ctx.method_name,
499
+ "sensitive_parameters": ", ".join(found_sensitive),
500
+ "violation_type": "credential_in_signature",
501
+ },
502
+ )
503
+ )
209
504
 
210
- for param_name in SENSITIVE_PARAMETER_NAMES:
211
- # Match parameter name as word boundary to avoid false positives
212
- # e.g., "password" matches but "password_hash" doesn't
213
- pattern = rf"\b{param_name}\b"
214
- if re.search(pattern, signature_lower):
215
- found_sensitive.append(param_name)
505
+ return issues
216
506
 
217
- return found_sensitive
218
507
 
508
+ # CLI entry point
509
+ if __name__ == "__main__":
510
+ sys.exit(ValidatorSecurity.main())
219
511
 
220
- def validate_method_exposure(
221
- method_names: list[str],
222
- handler_identity: ModelHandlerIdentifier,
223
- method_signatures: dict[str, str] | None = None,
224
- file_path: str | None = None,
225
- ) -> list[ModelHandlerValidationError]:
226
- """Validate method names and signatures for security concerns.
227
512
 
228
- Checks for:
229
- - Sensitive method names that should be private (prefixed with _)
230
- - Method signatures containing sensitive parameter names
231
- - Admin/internal operations exposed publicly
232
-
233
- Args:
234
- method_names: List of public method names to validate
235
- handler_identity: Handler identification information
236
- method_signatures: Optional dict mapping method names to signatures
237
- file_path: Optional file path for error context
238
-
239
- Returns:
240
- List of ModelHandlerValidationError for any violations found
241
-
242
- Example:
243
- >>> from omnibase_infra.models.handlers import ModelHandlerIdentifier
244
- >>> identity = ModelHandlerIdentifier.from_handler_id("auth-handler")
245
- >>> errors = validate_method_exposure(
246
- ... method_names=["get_api_key", "process_request"],
247
- ... handler_identity=identity,
248
- ... method_signatures={"get_api_key": "() -> str"},
249
- ... )
250
- >>> len(errors)
251
- 1
252
- >>> errors[0].rule_id
253
- 'SECURITY-001'
254
- """
255
- errors: list[ModelHandlerValidationError] = []
256
-
257
- for method_name in method_names:
258
- # Check for sensitive method names and get appropriate rule ID
259
- rule_id = _get_sensitivity_rule_id(method_name)
260
- if rule_id is not None:
261
- # Customize message and hint based on rule type
262
- if rule_id == SecurityRuleId.ADMIN_METHOD_PUBLIC:
263
- message = f"Handler exposes admin/internal method '{method_name}'"
264
- remediation_hint = f"Prefix method with underscore: '_{method_name}' or move to separate admin module"
265
- violation_type = "admin_method_public"
266
- else:
267
- message = f"Handler exposes sensitive method '{method_name}'"
268
- remediation_hint = f"Prefix method with underscore: '_{method_name}' to exclude from introspection"
269
- violation_type = "sensitive_method_exposed"
270
-
271
- error = ModelHandlerValidationError.from_security_violation(
272
- rule_id=rule_id,
273
- message=message,
274
- remediation_hint=remediation_hint,
275
- handler_identity=handler_identity,
276
- file_path=file_path,
277
- details={
278
- "method_name": method_name,
279
- "violation_type": violation_type,
280
- },
281
- )
282
- errors.append(error)
283
-
284
- # Check method signatures if provided
285
- if method_signatures:
286
- for method_name, signature in method_signatures.items():
287
- sensitive_params = has_sensitive_parameters(signature)
288
- if sensitive_params:
289
- error = ModelHandlerValidationError.from_security_violation(
290
- rule_id=SecurityRuleId.CREDENTIAL_IN_SIGNATURE,
291
- message=f"Method '{method_name}' has sensitive parameters in signature: {', '.join(sensitive_params)}",
292
- remediation_hint=f"Use generic parameter names (e.g., 'data' instead of '{sensitive_params[0]}') or make method private",
293
- handler_identity=handler_identity,
294
- file_path=file_path,
295
- details={
296
- "method_name": method_name,
297
- "signature": signature,
298
- "sensitive_parameters": sensitive_params,
299
- "violation_type": "credential_in_signature",
300
- },
301
- )
302
- errors.append(error)
303
-
304
- return errors
305
-
306
-
307
- def validate_handler_security(
308
- handler_identity: ModelHandlerIdentifier,
309
- capabilities: dict[str, object],
310
- file_path: str | None = None,
311
- ) -> list[ModelHandlerValidationError]:
312
- """Validate handler capabilities for security violations.
313
-
314
- This is the main entry point for security validation of a handler's
315
- introspection data. It checks:
316
- - Method exposure (sensitive method names)
317
- - Method signatures (sensitive parameters)
318
- - Security best practices
319
-
320
- Args:
321
- handler_identity: Handler identification information
322
- capabilities: Capabilities dict from MixinNodeIntrospection.get_capabilities()
323
- file_path: Optional file path for error context
324
-
325
- Returns:
326
- List of ModelHandlerValidationError for any violations found
327
-
328
- Example:
329
- >>> capabilities = {
330
- ... "operations": ["get_api_key", "process_request"],
331
- ... "protocols": ["ProtocolDatabaseAdapter"],
332
- ... "has_fsm": False,
333
- ... "method_signatures": {
334
- ... "get_api_key": "() -> str",
335
- ... "process_request": "(data: dict) -> Result",
336
- ... },
337
- ... }
338
- >>> errors = validate_handler_security(
339
- ... handler_identity=ModelHandlerIdentifier.from_handler_id("auth"),
340
- ... capabilities=capabilities,
341
- ... )
342
- >>> len(errors) > 0
343
- True
344
- """
345
- operations = capabilities.get("operations", [])
346
- if not isinstance(operations, list):
347
- operations = []
348
-
349
- method_signatures = capabilities.get("method_signatures", {})
350
- if not isinstance(method_signatures, dict):
351
- method_signatures = {}
352
-
353
- return validate_method_exposure(
354
- method_names=operations,
355
- handler_identity=handler_identity,
356
- method_signatures=method_signatures,
357
- file_path=file_path,
358
- )
359
-
360
-
361
- def convert_to_validation_error(
362
- params: ModelValidationErrorParams,
363
- ) -> ModelHandlerValidationError:
364
- """Convert a security issue to ModelHandlerValidationError.
365
-
366
- This is a convenience function for creating security validation errors
367
- with consistent structure. Use this when integrating security checks
368
- into existing validation pipelines.
369
-
370
- Args:
371
- params: Parameters encapsulating rule_id, message, remediation_hint,
372
- handler_identity, and optional file_path, line_number, details.
373
-
374
- Returns:
375
- ModelHandlerValidationError configured for security violations
376
-
377
- Example:
378
- >>> params = ModelValidationErrorParams(
379
- ... rule_code=SecurityRuleId.SENSITIVE_METHOD_EXPOSED,
380
- ... message="Handler exposes 'get_password' method",
381
- ... remediation_hint="Prefix with underscore: '_get_password'",
382
- ... handler_identity=ModelHandlerIdentifier.from_handler_id("auth"),
383
- ... file_path="nodes/auth/handlers/handler_authenticate.py",
384
- ... line_number=42,
385
- ... )
386
- >>> error = convert_to_validation_error(params)
387
- >>> error.error_type == EnumHandlerErrorType.SECURITY_VALIDATION_ERROR
388
- True
389
- """
390
- return ModelHandlerValidationError.from_security_violation(
391
- rule_id=params.rule_code,
392
- message=params.message,
393
- remediation_hint=params.remediation_hint,
394
- handler_identity=params.handler_identity,
395
- file_path=params.file_path,
396
- line_number=params.line_number,
397
- details=params.details,
398
- )
399
-
400
-
401
- __all__ = [
402
- "SENSITIVE_METHOD_PATTERNS",
403
- "SENSITIVE_PARAMETER_NAMES",
404
- "SecurityRuleId",
405
- "convert_to_validation_error",
406
- "has_sensitive_parameters",
407
- "is_sensitive_method_name",
408
- "validate_handler_security",
409
- "validate_method_exposure",
410
- ]
513
+ __all__ = ["ValidatorSecurity"]