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.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
- omnibase_infra/capabilities/__init__.py +15 -0
- omnibase_infra/capabilities/capability_inference_rules.py +211 -0
- omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
- omnibase_infra/capabilities/intent_type_extractor.py +160 -0
- omnibase_infra/cli/commands.py +1 -1
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +5 -2
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +5 -2
- omnibase_infra/enums/__init__.py +6 -0
- omnibase_infra/enums/enum_handler_error_type.py +10 -0
- omnibase_infra/enums/enum_handler_source_mode.py +72 -0
- omnibase_infra/enums/enum_kafka_acks.py +99 -0
- omnibase_infra/errors/error_compute_registry.py +4 -1
- omnibase_infra/errors/error_event_bus_registry.py +4 -1
- omnibase_infra/errors/error_infra.py +3 -1
- omnibase_infra/errors/error_policy_registry.py +4 -1
- omnibase_infra/event_bus/event_bus_kafka.py +1 -1
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +59 -10
- omnibase_infra/handlers/__init__.py +8 -1
- omnibase_infra/handlers/handler_consul.py +7 -1
- omnibase_infra/handlers/handler_db.py +10 -3
- omnibase_infra/handlers/handler_graph.py +10 -5
- omnibase_infra/handlers/handler_http.py +8 -2
- omnibase_infra/handlers/handler_intent.py +387 -0
- omnibase_infra/handlers/handler_mcp.py +745 -63
- omnibase_infra/handlers/handler_vault.py +11 -5
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
- omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +7 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +308 -4
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
- omnibase_infra/mixins/mixin_node_introspection.py +42 -7
- omnibase_infra/mixins/mixin_retry_execution.py +1 -1
- omnibase_infra/models/discovery/model_introspection_config.py +11 -0
- omnibase_infra/models/handlers/__init__.py +48 -5
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +6 -4
- omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
- omnibase_infra/models/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/registration/model_node_capabilities.py +11 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +9 -0
- omnibase_infra/models/runtime/model_handler_contract.py +25 -9
- omnibase_infra/models/runtime/model_loaded_handler.py +9 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
- omnibase_infra/nodes/effects/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
- omnibase_infra/nodes/node_registration_orchestrator/plugin.py +1 -1
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +4 -3
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_storage_effect/node.py +4 -1
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +47 -26
- omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +28 -20
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
- omnibase_infra/plugins/plugin_compute_base.py +16 -2
- omnibase_infra/protocols/__init__.py +2 -0
- omnibase_infra/protocols/protocol_container_aware.py +200 -0
- omnibase_infra/protocols/protocol_event_projector.py +1 -1
- omnibase_infra/runtime/__init__.py +90 -1
- omnibase_infra/runtime/binding_config_resolver.py +102 -37
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/contract_handler_discovery.py +6 -1
- omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +267 -186
- omnibase_infra/runtime/handler_identity.py +81 -0
- omnibase_infra/runtime/handler_plugin_loader.py +19 -2
- omnibase_infra/runtime/handler_registry.py +11 -3
- omnibase_infra/runtime/handler_source_resolver.py +326 -0
- omnibase_infra/runtime/mixin_semver_cache.py +25 -1
- omnibase_infra/runtime/mixins/__init__.py +7 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
- omnibase_infra/runtime/models/__init__.py +24 -0
- omnibase_infra/runtime/models/model_health_check_result.py +2 -1
- omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1 -1
- omnibase_infra/runtime/projector_shell.py +229 -1
- omnibase_infra/runtime/protocol_lifecycle_executor.py +6 -6
- omnibase_infra/runtime/protocols/__init__.py +10 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +16 -15
- omnibase_infra/runtime/registry_contract_source.py +693 -0
- omnibase_infra/runtime/registry_policy.py +9 -326
- omnibase_infra/runtime/secret_resolver.py +4 -2
- omnibase_infra/runtime/service_kernel.py +11 -3
- omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
- omnibase_infra/runtime/service_runtime_host_process.py +589 -106
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +764 -0
- omnibase_infra/runtime/util_container_wiring.py +6 -5
- omnibase_infra/runtime/util_wiring.py +17 -4
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/__init__.py +21 -0
- omnibase_infra/services/corpus_capture.py +7 -1
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +261 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +837 -0
- omnibase_infra/services/service_capability_query.py +4 -4
- omnibase_infra/services/service_health.py +3 -2
- omnibase_infra/services/service_timeout_emitter.py +20 -3
- omnibase_infra/services/service_timeout_scanner.py +7 -3
- omnibase_infra/services/session/__init__.py +56 -0
- omnibase_infra/services/session/config_consumer.py +120 -0
- omnibase_infra/services/session/config_store.py +139 -0
- omnibase_infra/services/session/consumer.py +1007 -0
- omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
- omnibase_infra/services/session/store.py +997 -0
- omnibase_infra/utils/__init__.py +19 -0
- omnibase_infra/utils/util_atomic_file.py +261 -0
- omnibase_infra/utils/util_db_transaction.py +239 -0
- omnibase_infra/utils/util_dsn_validation.py +1 -1
- omnibase_infra/utils/util_retry_optimistic.py +281 -0
- omnibase_infra/validation/__init__.py +3 -19
- omnibase_infra/validation/contracts/security.validation.yaml +114 -0
- omnibase_infra/validation/infra_validators.py +35 -24
- omnibase_infra/validation/validation_exemptions.yaml +140 -9
- omnibase_infra/validation/validator_chain_propagation.py +2 -2
- omnibase_infra/validation/validator_runtime_shape.py +1 -1
- omnibase_infra/validation/validator_security.py +473 -370
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/RECORD +161 -98
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/entry_points.txt +0 -0
- {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
|
|
3
|
+
"""Security Validator for ONEX Infrastructure.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
|
27
|
-
|
|
28
|
-
... validate_handler_security,
|
|
29
|
-
... )
|
|
15
|
+
>>> from pathlib import Path
|
|
16
|
+
>>> from omnibase_infra.validation.validator_security import ValidatorSecurity
|
|
30
17
|
>>>
|
|
31
|
-
>>>
|
|
32
|
-
>>>
|
|
33
|
-
|
|
34
|
-
...
|
|
35
|
-
... )
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
from
|
|
54
|
-
from
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
144
|
-
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class CompiledPatterns:
|
|
54
|
+
"""Pre-compiled regex patterns for security validation.
|
|
145
55
|
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
171
|
-
|
|
85
|
+
class ValidatorSecurity(ValidatorBase):
|
|
86
|
+
"""Contract-driven security validator for Python source files.
|
|
172
87
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"]
|