olyx-guardrails 1.0.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +117 -0
- data/CODE_OF_CONDUCT.md +28 -0
- data/CONTRIBUTING.md +63 -0
- data/LICENSE +134 -0
- data/README.md +611 -0
- data/SECURITY.md +35 -0
- data/docs/API.md +460 -0
- data/examples/claude_analyzer.rb +53 -0
- data/examples/custom_policy.rb +41 -0
- data/examples/notifier.rb +37 -0
- data/examples/openai_analyzer.rb +45 -0
- data/examples/rails_opt_in.rb +99 -0
- data/examples/ruby_only.rb +53 -0
- data/lib/generators/olyx_guardrails/install_generator.rb +22 -0
- data/lib/generators/olyx_guardrails/templates/initializer.rb.tt +16 -0
- data/lib/generators/olyx_guardrails/templates/policy.yml.tt +41 -0
- data/lib/olyx/guardrails/ai/analysis_normalizer.rb +26 -0
- data/lib/olyx/guardrails/ai/analysis_pipeline.rb +26 -0
- data/lib/olyx/guardrails/ai/boolean_validator.rb +18 -0
- data/lib/olyx/guardrails/ai/flag_finding_merger.rb +16 -0
- data/lib/olyx/guardrails/ai/result_sanitizer.rb +29 -0
- data/lib/olyx/guardrails/ai/secret_finding_merger.rb +19 -0
- data/lib/olyx/guardrails/ai/standard_finding_merger.rb +24 -0
- data/lib/olyx/guardrails/ai_analysis.rb +29 -0
- data/lib/olyx/guardrails/ai_context_builder.rb +32 -0
- data/lib/olyx/guardrails/ai_failure_handler.rb +17 -0
- data/lib/olyx/guardrails/ai_finding_merger.rb +30 -0
- data/lib/olyx/guardrails/check_analyzer.rb +26 -0
- data/lib/olyx/guardrails/check_base_result.rb +32 -0
- data/lib/olyx/guardrails/check_pipeline.rb +17 -0
- data/lib/olyx/guardrails/check_result_builder.rb +24 -0
- data/lib/olyx/guardrails/check_runner.rb +42 -0
- data/lib/olyx/guardrails/check_set.rb +33 -0
- data/lib/olyx/guardrails/checks/injection_check.rb +20 -0
- data/lib/olyx/guardrails/checks/injection_result.rb +22 -0
- data/lib/olyx/guardrails/checks/length_check.rb +18 -0
- data/lib/olyx/guardrails/checks/message_injection_check.rb +19 -0
- data/lib/olyx/guardrails/checks/pii_check.rb +19 -0
- data/lib/olyx/guardrails/checks/policy_check.rb +26 -0
- data/lib/olyx/guardrails/checks/secret_check.rb +30 -0
- data/lib/olyx/guardrails/checks/skipped_checks.rb +26 -0
- data/lib/olyx/guardrails/enum_value.rb +20 -0
- data/lib/olyx/guardrails/errors.rb +22 -0
- data/lib/olyx/guardrails/injection_detector.rb +27 -0
- data/lib/olyx/guardrails/injection_patterns.rb +44 -0
- data/lib/olyx/guardrails/integrations/openai/analyzer_setup.rb +21 -0
- data/lib/olyx/guardrails/integrations/openai/configuration_resolver.rb +28 -0
- data/lib/olyx/guardrails/integrations/openai/dependency_contracts.rb +28 -0
- data/lib/olyx/guardrails/integrations/openai/input_builder.rb +39 -0
- data/lib/olyx/guardrails/integrations/openai/member_reader.rb +21 -0
- data/lib/olyx/guardrails/integrations/openai/model_identifier.rb +28 -0
- data/lib/olyx/guardrails/integrations/openai/refusal_guard.rb +28 -0
- data/lib/olyx/guardrails/integrations/openai/request_builder.rb +36 -0
- data/lib/olyx/guardrails/integrations/openai/request_configuration.rb +25 -0
- data/lib/olyx/guardrails/integrations/openai/request_values.rb +33 -0
- data/lib/olyx/guardrails/integrations/openai/response_contents.rb +22 -0
- data/lib/olyx/guardrails/integrations/openai/response_option_keys.rb +32 -0
- data/lib/olyx/guardrails/integrations/openai/response_options.rb +23 -0
- data/lib/olyx/guardrails/integrations/openai/schema_registry.rb +46 -0
- data/lib/olyx/guardrails/integrations/openai/sdk.rb +26 -0
- data/lib/olyx/guardrails/integrations/openai/signal_summary.rb +40 -0
- data/lib/olyx/guardrails/integrations/openai_analyzer.rb +107 -0
- data/lib/olyx/guardrails/integrations/openai_analyzer_configuration.rb +38 -0
- data/lib/olyx/guardrails/integrations/openai_response_parser.rb +34 -0
- data/lib/olyx/guardrails/message_check_runner.rb +24 -0
- data/lib/olyx/guardrails/message_check_set.rb +20 -0
- data/lib/olyx/guardrails/message_content.rb +27 -0
- data/lib/olyx/guardrails/message_source.rb +16 -0
- data/lib/olyx/guardrails/multi_turn_injection_detector.rb +18 -0
- data/lib/olyx/guardrails/multi_turn_pair_scanner.rb +35 -0
- data/lib/olyx/guardrails/multi_turn_variant_pairs.rb +25 -0
- data/lib/olyx/guardrails/notification/base_event.rb +35 -0
- data/lib/olyx/guardrails/notification/deep_freezer.rb +25 -0
- data/lib/olyx/guardrails/notification/delivery_dispatcher.rb +38 -0
- data/lib/olyx/guardrails/notification/delivery_summary.rb +17 -0
- data/lib/olyx/guardrails/notification/handler.rb +33 -0
- data/lib/olyx/guardrails/notification/handler_collection.rb +25 -0
- data/lib/olyx/guardrails/notification/handler_collection_validator.rb +23 -0
- data/lib/olyx/guardrails/notification/key.rb +19 -0
- data/lib/olyx/guardrails/notification/metadata.rb +34 -0
- data/lib/olyx/guardrails/notification/notifier_setup.rb +19 -0
- data/lib/olyx/guardrails/notification/preview.rb +27 -0
- data/lib/olyx/guardrails/notification/scrubber.rb +22 -0
- data/lib/olyx/guardrails/notification/violation_labels.rb +19 -0
- data/lib/olyx/guardrails/notification_event_builder.rb +41 -0
- data/lib/olyx/guardrails/notification_sanitizer.rb +33 -0
- data/lib/olyx/guardrails/notifier.rb +53 -0
- data/lib/olyx/guardrails/notifier_configuration.rb +30 -0
- data/lib/olyx/guardrails/pii/ambiguous_numeric_date.rb +32 -0
- data/lib/olyx/guardrails/pii/block_scrubber.rb +31 -0
- data/lib/olyx/guardrails/pii/content_scrubber.rb +33 -0
- data/lib/olyx/guardrails/pii/date_validator.rb +25 -0
- data/lib/olyx/guardrails/pii/hash_key.rb +19 -0
- data/lib/olyx/guardrails/pii/iban_remainder.rb +25 -0
- data/lib/olyx/guardrails/pii/iban_validator.rb +34 -0
- data/lib/olyx/guardrails/pii/ipv4_validator.rb +17 -0
- data/lib/olyx/guardrails/pii/ipv6_validator.rb +20 -0
- data/lib/olyx/guardrails/pii/luhn_checksum.rb +29 -0
- data/lib/olyx/guardrails/pii/luhn_validator.rb +21 -0
- data/lib/olyx/guardrails/pii/message_scrubber.rb +31 -0
- data/lib/olyx/guardrails/pii/named_date_validator.rb +28 -0
- data/lib/olyx/guardrails/pii/numeric_date_validator.rb +26 -0
- data/lib/olyx/guardrails/pii/pattern_catalog.rb +41 -0
- data/lib/olyx/guardrails/pii/sin_validator.rb +24 -0
- data/lib/olyx/guardrails/pii/ssn_validator.rb +21 -0
- data/lib/olyx/guardrails/pii/text_scrubber.rb +28 -0
- data/lib/olyx/guardrails/pii/year_first_date.rb +18 -0
- data/lib/olyx/guardrails/pii_scrubber.rb +23 -0
- data/lib/olyx/guardrails/pii_validators.rb +26 -0
- data/lib/olyx/guardrails/policy/ai_failure_mode.rb +15 -0
- data/lib/olyx/guardrails/policy/configuration.rb +62 -0
- data/lib/olyx/guardrails/policy/configuration_hash.rb +20 -0
- data/lib/olyx/guardrails/policy/finding_order.rb +20 -0
- data/lib/olyx/guardrails/policy/finding_presenter.rb +41 -0
- data/lib/olyx/guardrails/policy/match_collector.rb +20 -0
- data/lib/olyx/guardrails/policy/name.rb +23 -0
- data/lib/olyx/guardrails/policy/normalized_pattern_matcher.rb +34 -0
- data/lib/olyx/guardrails/policy/pattern_matcher.rb +19 -0
- data/lib/olyx/guardrails/policy/redaction_result.rb +20 -0
- data/lib/olyx/guardrails/policy/redaction_spans.rb +32 -0
- data/lib/olyx/guardrails/policy/rule_collection.rb +34 -0
- data/lib/olyx/guardrails/policy/rule_matcher.rb +29 -0
- data/lib/olyx/guardrails/policy/rule_normalizer.rb +21 -0
- data/lib/olyx/guardrails/policy/scan_result.rb +21 -0
- data/lib/olyx/guardrails/policy/secret_pattern_collection.rb +22 -0
- data/lib/olyx/guardrails/policy/unmatched_segment_builder.rb +28 -0
- data/lib/olyx/guardrails/policy/unmatched_transformer.rb +20 -0
- data/lib/olyx/guardrails/policy.rb +46 -0
- data/lib/olyx/guardrails/policy_aware_redactor.rb +42 -0
- data/lib/olyx/guardrails/policy_redactor.rb +28 -0
- data/lib/olyx/guardrails/policy_rule/configuration.rb +46 -0
- data/lib/olyx/guardrails/policy_rule/description_value.rb +24 -0
- data/lib/olyx/guardrails/policy_rule/match_mode.rb +15 -0
- data/lib/olyx/guardrails/policy_rule/name_value.rb +22 -0
- data/lib/olyx/guardrails/policy_rule/pattern_compiler.rb +28 -0
- data/lib/olyx/guardrails/policy_rule/regexp_compiler.rb +31 -0
- data/lib/olyx/guardrails/policy_rule/replacement_value.rb +26 -0
- data/lib/olyx/guardrails/policy_rule/term_compiler.rb +33 -0
- data/lib/olyx/guardrails/policy_rule/text_values.rb +23 -0
- data/lib/olyx/guardrails/policy_rule/values.rb +33 -0
- data/lib/olyx/guardrails/policy_rule.rb +30 -0
- data/lib/olyx/guardrails/policy_scanner.rb +44 -0
- data/lib/olyx/guardrails/rails/action_cable.rb +21 -0
- data/lib/olyx/guardrails/rails/active_job_handler.rb +27 -0
- data/lib/olyx/guardrails/rails/active_model_validator.rb +23 -0
- data/lib/olyx/guardrails/rails/check_telemetry.rb +19 -0
- data/lib/olyx/guardrails/rails/configuration.rb +68 -0
- data/lib/olyx/guardrails/rails/configuration_finalizer.rb +30 -0
- data/lib/olyx/guardrails/rails/configuration_registry.rb +41 -0
- data/lib/olyx/guardrails/rails/configuration_values.rb +30 -0
- data/lib/olyx/guardrails/rails/constant_resolver.rb +18 -0
- data/lib/olyx/guardrails/rails/controller.rb +34 -0
- data/lib/olyx/guardrails/rails/controller_metadata.rb +22 -0
- data/lib/olyx/guardrails/rails/decision_service.rb +26 -0
- data/lib/olyx/guardrails/rails/enforcer.rb +33 -0
- data/lib/olyx/guardrails/rails/evaluation_lifecycle.rb +23 -0
- data/lib/olyx/guardrails/rails/evaluation_service.rb +12 -0
- data/lib/olyx/guardrails/rails/filter_parameters.rb +26 -0
- data/lib/olyx/guardrails/rails/graphql.rb +21 -0
- data/lib/olyx/guardrails/rails/ingress.rb +26 -0
- data/lib/olyx/guardrails/rails/input_runtime.rb +25 -0
- data/lib/olyx/guardrails/rails/instrumentation.rb +39 -0
- data/lib/olyx/guardrails/rails/instrumentation_payloads.rb +25 -0
- data/lib/olyx/guardrails/rails/instrumentation_publisher.rb +24 -0
- data/lib/olyx/guardrails/rails/integration_configuration.rb +27 -0
- data/lib/olyx/guardrails/rails/job.rb +37 -0
- data/lib/olyx/guardrails/rails/job_argument.rb +41 -0
- data/lib/olyx/guardrails/rails/job_reference.rb +36 -0
- data/lib/olyx/guardrails/rails/message_evaluation_service.rb +13 -0
- data/lib/olyx/guardrails/rails/message_runtime.rb +20 -0
- data/lib/olyx/guardrails/rails/notification_dispatcher.rb +22 -0
- data/lib/olyx/guardrails/rails/output_runtime.rb +26 -0
- data/lib/olyx/guardrails/rails/path_value.rb +30 -0
- data/lib/olyx/guardrails/rails/policy_configuration.rb +25 -0
- data/lib/olyx/guardrails/rails/policy_document.rb +40 -0
- data/lib/olyx/guardrails/rails/policy_file.rb +29 -0
- data/lib/olyx/guardrails/rails/policy_file_error.rb +25 -0
- data/lib/olyx/guardrails/rails/policy_rule_names.rb +19 -0
- data/lib/olyx/guardrails/rails/policy_yaml.rb +22 -0
- data/lib/olyx/guardrails/rails/queue_name.rb +21 -0
- data/lib/olyx/guardrails/rails/redaction_payload.rb +36 -0
- data/lib/olyx/guardrails/rails/redaction_service.rb +21 -0
- data/lib/olyx/guardrails/rails/result_summary.rb +36 -0
- data/lib/olyx/guardrails/rails/runtime.rb +30 -0
- data/lib/olyx/guardrails/rails/timer.rb +27 -0
- data/lib/olyx/guardrails/rails/upload.rb +34 -0
- data/lib/olyx/guardrails/rails.rb +35 -0
- data/lib/olyx/guardrails/railtie.rb +19 -0
- data/lib/olyx/guardrails/redaction/content_result.rb +32 -0
- data/lib/olyx/guardrails/redaction/input_validator.rb +17 -0
- data/lib/olyx/guardrails/redaction/public_result.rb +23 -0
- data/lib/olyx/guardrails/redaction/unrestricted_text.rb +17 -0
- data/lib/olyx/guardrails/redactor.rb +29 -0
- data/lib/olyx/guardrails/risk/ai_score.rb +19 -0
- data/lib/olyx/guardrails/risk/check_weights.rb +18 -0
- data/lib/olyx/guardrails/risk/deterministic_score.rb +20 -0
- data/lib/olyx/guardrails/risk/weights.rb +18 -0
- data/lib/olyx/guardrails/risk_scorer.rb +28 -0
- data/lib/olyx/guardrails/secret_finding_collector.rb +31 -0
- data/lib/olyx/guardrails/secret_scanner.rb +42 -0
- data/lib/olyx/guardrails/secrets/blocked.rb +17 -0
- data/lib/olyx/guardrails/secrets/catalog_source.rb +28 -0
- data/lib/olyx/guardrails/secrets/cloud_credential_patterns.rb +16 -0
- data/lib/olyx/guardrails/secrets/confidentiality_source.rb +21 -0
- data/lib/olyx/guardrails/secrets/custom_pattern_compiler.rb +23 -0
- data/lib/olyx/guardrails/secrets/custom_pattern_source.rb +30 -0
- data/lib/olyx/guardrails/secrets/finding_order.rb +17 -0
- data/lib/olyx/guardrails/secrets/finding_presenter.rb +34 -0
- data/lib/olyx/guardrails/secrets/generic_credential_patterns.rb +15 -0
- data/lib/olyx/guardrails/secrets/internal_endpoint_source.rb +28 -0
- data/lib/olyx/guardrails/secrets/network_patterns.rb +13 -0
- data/lib/olyx/guardrails/secrets/normalized_source_set.rb +27 -0
- data/lib/olyx/guardrails/secrets/pattern_catalog.rb +37 -0
- data/lib/olyx/guardrails/secrets/private_network_validator.rb +19 -0
- data/lib/olyx/guardrails/secrets/redaction_spans.rb +32 -0
- data/lib/olyx/guardrails/secrets/redactor.rb +27 -0
- data/lib/olyx/guardrails/secrets/regexp_finding_source.rb +21 -0
- data/lib/olyx/guardrails/secrets/source_set.rb +19 -0
- data/lib/olyx/guardrails/single_message_injection_detector.rb +35 -0
- data/lib/olyx/guardrails/supplemental_violation_labels.rb +22 -0
- data/lib/olyx/guardrails/text/base64_decoder.rb +45 -0
- data/lib/olyx/guardrails/text/detection_variants.rb +28 -0
- data/lib/olyx/guardrails/text/html_decoder.rb +18 -0
- data/lib/olyx/guardrails/text/mapped_builder.rb +33 -0
- data/lib/olyx/guardrails/text/mapped_normalization.rb +27 -0
- data/lib/olyx/guardrails/text/normalizer.rb +35 -0
- data/lib/olyx/guardrails/text/unicode_escape_decoder.rb +20 -0
- data/lib/olyx/guardrails/text/url_decoder.rb +20 -0
- data/lib/olyx/guardrails/validation.rb +33 -0
- data/lib/olyx/guardrails/version.rb +8 -0
- data/lib/olyx/guardrails/violation_labels.rb +24 -0
- data/lib/olyx/guardrails.rb +63 -0
- metadata +283 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../message_source'
|
|
4
|
+
require_relative 'decision_service'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module Rails
|
|
9
|
+
# Evaluates structured messages with Rails telemetry and notifications.
|
|
10
|
+
MessageEvaluationService = DecisionService.new(:check_messages, notification_input: MessageSource)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'message_evaluation_service'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Rails
|
|
8
|
+
# Executes configured checks for structured conversation messages.
|
|
9
|
+
class MessageRuntime
|
|
10
|
+
def initialize(registry)
|
|
11
|
+
@registry = registry
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def check_messages(messages, metadata: {})
|
|
15
|
+
MessageEvaluationService.call(messages, metadata: metadata, configuration: @registry.ready)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'instrumentation'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Rails
|
|
8
|
+
# Dispatches optional Rails notifications without coupling evaluation.
|
|
9
|
+
module NotificationDispatcher
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(configuration, result, input, metadata)
|
|
13
|
+
notifier = configuration.notifier
|
|
14
|
+
return unless notifier
|
|
15
|
+
|
|
16
|
+
delivery = notifier.notify(result, input: input, metadata: metadata)
|
|
17
|
+
Instrumentation.publish_notification(delivery) if delivery
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'evaluation_service'
|
|
4
|
+
require_relative 'redaction_service'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module Rails
|
|
9
|
+
# Executes configured checks and redaction for completed model output.
|
|
10
|
+
class OutputRuntime
|
|
11
|
+
def initialize(registry)
|
|
12
|
+
@registry = registry
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def check_output(output, metadata: {})
|
|
16
|
+
context = { direction: 'output' }.merge(metadata)
|
|
17
|
+
EvaluationService.call(output, metadata: context, configuration: @registry.ready)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def redact_output(output)
|
|
21
|
+
RedactionService.call(output, configuration: @registry.ready)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Rails
|
|
6
|
+
# Normalizes an optional path-like configuration value.
|
|
7
|
+
module PathValue
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(value)
|
|
11
|
+
return nil if value.nil?
|
|
12
|
+
|
|
13
|
+
normalized = normalize(value)
|
|
14
|
+
return normalized.dup.freeze if valid?(normalized)
|
|
15
|
+
|
|
16
|
+
raise ArgumentError, 'Rails policy_path must be a path-like value'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def normalize(value)
|
|
20
|
+
value.respond_to?(:to_path) ? value.to_path : value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def valid?(value)
|
|
24
|
+
value.is_a?(String) && !value.empty?
|
|
25
|
+
end
|
|
26
|
+
private_class_method :normalize, :valid?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Rails
|
|
6
|
+
# Owns policy and filtering assignments during Rails boot.
|
|
7
|
+
module PolicyConfiguration
|
|
8
|
+
def policy=(value)
|
|
9
|
+
mutable!
|
|
10
|
+
@policy = ConfigurationValues.policy(value)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def policy_path=(value)
|
|
14
|
+
mutable!
|
|
15
|
+
@policy_path = ConfigurationValues.path(value)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def filter_parameters=(value)
|
|
19
|
+
mutable!
|
|
20
|
+
@filter_parameters = ConfigurationValues.filter_parameters(value)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'policy_yaml'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Rails
|
|
8
|
+
# Parses and selects direct or environment-keyed policy YAML.
|
|
9
|
+
class PolicyDocument
|
|
10
|
+
POLICY_KEYS = %w[name max_input_length block_pii block_injections block_secrets ai_failure_mode secret_patterns
|
|
11
|
+
rules].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(path, environment)
|
|
14
|
+
@path = path
|
|
15
|
+
@environment = environment
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
document = PolicyYaml.load(@path)
|
|
20
|
+
return document if direct?(document)
|
|
21
|
+
|
|
22
|
+
environment_policy(document)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def environment_policy(document)
|
|
28
|
+
selected = document[@environment] || document[@environment.to_sym]
|
|
29
|
+
return selected if selected.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
raise ConfigurationError, "guardrail policy file has no #{@environment.inspect} policy: #{@path}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def direct?(document)
|
|
35
|
+
document.keys.any? { |key| POLICY_KEYS.include?(key.to_s) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../policy'
|
|
4
|
+
require_relative 'policy_document'
|
|
5
|
+
require_relative 'policy_file_error'
|
|
6
|
+
|
|
7
|
+
module Olyx
|
|
8
|
+
module Guardrails
|
|
9
|
+
module Rails
|
|
10
|
+
# Safely loads direct or environment-keyed policy YAML without ERB.
|
|
11
|
+
class PolicyFile
|
|
12
|
+
def self.load(path, environment:)
|
|
13
|
+
new(path, environment).load
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(path, environment)
|
|
17
|
+
@path = path.to_s
|
|
18
|
+
@environment = environment.to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load
|
|
22
|
+
Policy.from_h(PolicyDocument.new(@path, @environment).call)
|
|
23
|
+
rescue Errno::ENOENT, Errno::EACCES, Psych::Exception, ArgumentError => error
|
|
24
|
+
raise PolicyFileError.call(error, @path)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Rails
|
|
6
|
+
# Translates policy loading failures into stable boot configuration errors.
|
|
7
|
+
module PolicyFileError
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(error, path)
|
|
11
|
+
ConfigurationError.new(message(error, path))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def message(error, path)
|
|
15
|
+
return "guardrail policy file does not exist: #{path}" if error.is_a?(Errno::ENOENT)
|
|
16
|
+
return "guardrail policy file is not readable: #{path}" if error.is_a?(Errno::EACCES)
|
|
17
|
+
return "guardrail policy file contains invalid or unsafe YAML: #{path}" if error.is_a?(Psych::Exception)
|
|
18
|
+
|
|
19
|
+
"invalid guardrail policy in #{path}: #{error.message}"
|
|
20
|
+
end
|
|
21
|
+
private_class_method :message
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Rails
|
|
6
|
+
# Extracts unique policy rule identifiers for safe telemetry.
|
|
7
|
+
module PolicyRuleNames
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(result)
|
|
11
|
+
Array(result[:policy_findings]).filter_map { |finding| name(finding) }.uniq.freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def name(finding) = finding[:rule]&.to_s
|
|
15
|
+
private_class_method :name
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Rails
|
|
8
|
+
# Safely reads a YAML policy document without aliases or object types.
|
|
9
|
+
module PolicyYaml
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def load(path)
|
|
13
|
+
document = YAML.safe_load_file(path, permitted_classes: [], permitted_symbols: [], aliases: false,
|
|
14
|
+
filename: path)
|
|
15
|
+
return document if document.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
raise ConfigurationError, "guardrail policy file must contain a Hash: #{path}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Rails
|
|
6
|
+
# Normalizes an optional Active Job queue name.
|
|
7
|
+
module QueueName
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(queue)
|
|
11
|
+
return nil if queue.nil?
|
|
12
|
+
|
|
13
|
+
name = queue.to_s
|
|
14
|
+
return name.freeze if (queue.is_a?(String) || queue.is_a?(Symbol)) && !name.empty?
|
|
15
|
+
|
|
16
|
+
raise ArgumentError, 'queue must be a non-empty String or Symbol'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Rails
|
|
6
|
+
# Builds content-free redaction telemetry.
|
|
7
|
+
module RedactionPayload
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(result, duration)
|
|
11
|
+
flags(result).merge(evaluation_duration_ms: duration).freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def flags(result)
|
|
15
|
+
identity(result).merge(detections(result))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def identity(result)
|
|
19
|
+
{
|
|
20
|
+
policy_name: result[:policy_name].to_s[0...100],
|
|
21
|
+
redacted: result[:redacted] == true
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def detections(result)
|
|
26
|
+
{
|
|
27
|
+
pii_detected: result[:pii_detected] == true,
|
|
28
|
+
secret_leaked: result[:secret_leaked] == true,
|
|
29
|
+
policy_violated: result[:policy_violated] == true
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
private_class_method :detections, :flags, :identity
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'instrumentation'
|
|
4
|
+
require_relative 'timer'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module Rails
|
|
9
|
+
# Runs one configured transformation and publishes safe telemetry.
|
|
10
|
+
module RedactionService
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(input, configuration:)
|
|
14
|
+
result, duration = Timer.call { Guardrails.redact(input, policy: configuration.policy) }
|
|
15
|
+
Instrumentation.publish_redaction(result, duration)
|
|
16
|
+
result
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../violation_labels'
|
|
4
|
+
require_relative '../notification/deep_freezer'
|
|
5
|
+
require_relative 'policy_rule_names'
|
|
6
|
+
|
|
7
|
+
module Olyx
|
|
8
|
+
module Guardrails
|
|
9
|
+
module Rails
|
|
10
|
+
# Builds the bounded decision payload used by exceptions and telemetry.
|
|
11
|
+
module ResultSummary
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(result)
|
|
15
|
+
Notification::DeepFreezer.call(decision(result).merge(policy(result)))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def decision(result)
|
|
19
|
+
{
|
|
20
|
+
policy_name: result[:policy_name].to_s[0...100],
|
|
21
|
+
allowed: result[:allowed] == true,
|
|
22
|
+
risk_score: result[:risk_score].to_f
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def policy(result)
|
|
27
|
+
{
|
|
28
|
+
violations: Guardrails::ViolationLabels.call(result).freeze,
|
|
29
|
+
policy_rules: PolicyRuleNames.call(result)
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
private_class_method :decision, :policy
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
require_relative 'configuration_registry'
|
|
5
|
+
require_relative 'input_runtime'
|
|
6
|
+
require_relative 'message_runtime'
|
|
7
|
+
require_relative 'output_runtime'
|
|
8
|
+
|
|
9
|
+
module Olyx
|
|
10
|
+
module Guardrails
|
|
11
|
+
module Rails
|
|
12
|
+
# Composes the independent configuration, input, message, and output runtimes.
|
|
13
|
+
class Runtime
|
|
14
|
+
extend Forwardable
|
|
15
|
+
|
|
16
|
+
def_delegators :@registry, :configuration, :configure, :finalize!
|
|
17
|
+
def_delegators :@input, :check, :redact
|
|
18
|
+
def_delegator :@messages, :check_messages
|
|
19
|
+
def_delegators :@output, :check_output, :redact_output
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@registry = ConfigurationRegistry.new
|
|
23
|
+
@input = InputRuntime.new(@registry)
|
|
24
|
+
@messages = MessageRuntime.new(@registry)
|
|
25
|
+
@output = OutputRuntime.new(@registry)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Rails
|
|
6
|
+
# Measures adapter operations with a monotonic clock.
|
|
7
|
+
module Timer
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
started = clock_time
|
|
12
|
+
result = yield
|
|
13
|
+
[result, elapsed_milliseconds(started)]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def clock_time
|
|
17
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def elapsed_milliseconds(started)
|
|
21
|
+
((clock_time - started) * 1_000).round(3)
|
|
22
|
+
end
|
|
23
|
+
private_class_method :clock_time, :elapsed_milliseconds
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'enforcer'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Rails
|
|
8
|
+
# Checks caller-extracted upload text without owning parsing or storage.
|
|
9
|
+
module Upload
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def check(upload, extractor:, metadata: {})
|
|
13
|
+
text = extract(upload, extractor)
|
|
14
|
+
Guardrails::Rails.check(text, metadata: { ingress: 'upload' }.merge(metadata))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def check!(upload, extractor:, metadata: {})
|
|
18
|
+
text = extract(upload, extractor)
|
|
19
|
+
Enforcer.check!(text, metadata: { ingress: 'upload' }.merge(metadata))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def extract(upload, extractor)
|
|
23
|
+
raise ArgumentError, 'upload extractor must respond to call' unless extractor.respond_to?(:call)
|
|
24
|
+
|
|
25
|
+
text = extractor.call(upload)
|
|
26
|
+
raise ArgumentError, 'upload extractor must return a String' unless text.is_a?(String)
|
|
27
|
+
|
|
28
|
+
text
|
|
29
|
+
end
|
|
30
|
+
private_class_method :extract
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/notifications'
|
|
4
|
+
require 'forwardable'
|
|
5
|
+
require_relative '../guardrails'
|
|
6
|
+
require_relative 'rails/active_job_handler'
|
|
7
|
+
require_relative 'rails/action_cable'
|
|
8
|
+
require_relative 'rails/active_model_validator'
|
|
9
|
+
require_relative 'rails/controller'
|
|
10
|
+
require_relative 'rails/enforcer'
|
|
11
|
+
require_relative 'rails/graphql'
|
|
12
|
+
require_relative 'rails/job'
|
|
13
|
+
require_relative 'rails/runtime'
|
|
14
|
+
require_relative 'rails/upload'
|
|
15
|
+
|
|
16
|
+
module Olyx
|
|
17
|
+
module Guardrails
|
|
18
|
+
# Optional Rails-native configuration and enforcement facade.
|
|
19
|
+
module Rails
|
|
20
|
+
extend SingleForwardable
|
|
21
|
+
|
|
22
|
+
def self.runtime
|
|
23
|
+
@runtime ||= Runtime.new
|
|
24
|
+
end
|
|
25
|
+
private_class_method :runtime
|
|
26
|
+
|
|
27
|
+
def_single_delegators :runtime,
|
|
28
|
+
:configuration, :configure, :finalize!,
|
|
29
|
+
:check, :redact, :check_messages,
|
|
30
|
+
:check_output, :redact_output
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
require_relative 'railtie' if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/railtie'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
# Boots and validates the optional Rails adapter.
|
|
8
|
+
class Railtie < ::Rails::Railtie
|
|
9
|
+
initializer 'olyx_guardrails.filter_parameters', after: :load_config_initializers do |app|
|
|
10
|
+
filters = Guardrails::Rails.configuration.filter_parameters
|
|
11
|
+
app.config.filter_parameters.concat(filters).uniq!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
config.after_initialize do
|
|
15
|
+
Guardrails::Rails.finalize!(environment: ::Rails.env)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Redaction
|
|
6
|
+
# Builds the stable internal redaction content contract.
|
|
7
|
+
module ContentResult
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(source, text:, secret_scan:, policy_redaction:)
|
|
11
|
+
findings = secret_scan[:findings]
|
|
12
|
+
detection(source, secret_scan, policy_redaction).merge(content(text, findings))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def content(text, findings)
|
|
16
|
+
output = '[REDACTED]' if findings.any? { |finding| finding[:category] == 'confidentiality_marker' }
|
|
17
|
+
{ text: output || text, findings: findings }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def detection(source, secret_scan, policy_redaction)
|
|
21
|
+
{
|
|
22
|
+
pii_detected: PiiScrubber.scrub(source) != source,
|
|
23
|
+
secret_leaked: secret_scan[:leaked],
|
|
24
|
+
policy_violated: policy_redaction[:violated],
|
|
25
|
+
policy_findings: policy_redaction[:findings]
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
private_class_method :content, :detection
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Redaction
|
|
6
|
+
# Validates redaction policy type and configured length limit.
|
|
7
|
+
module InputValidator
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(source, policy)
|
|
11
|
+
raise ArgumentError, 'policy must be an Olyx::Guardrails::Policy' unless policy.is_a?(Policy)
|
|
12
|
+
raise ArgumentError, 'input exceeds max_input_length' if source.length > policy.max_input_length
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Redaction
|
|
6
|
+
# Presents the stable public redaction result contract.
|
|
7
|
+
module PublicResult
|
|
8
|
+
CONTENT_FIELDS = %i[pii_detected secret_leaked policy_violated policy_findings findings].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(source, content, policy)
|
|
13
|
+
text = content[:text]
|
|
14
|
+
content.slice(*CONTENT_FIELDS).merge(
|
|
15
|
+
text: text,
|
|
16
|
+
redacted: text != source,
|
|
17
|
+
policy_name: policy.name
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Redaction
|
|
6
|
+
# Applies secret and PII transforms outside policy-owned spans.
|
|
7
|
+
module UnrestrictedText
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(text, policy)
|
|
11
|
+
secret_safe = SecretScanner.redact(text, custom_patterns: policy.secret_patterns)[:text]
|
|
12
|
+
PiiScrubber.scrub(secret_safe)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'policy'
|
|
4
|
+
require_relative 'policy_aware_redactor'
|
|
5
|
+
require_relative 'redaction/input_validator'
|
|
6
|
+
require_relative 'redaction/public_result'
|
|
7
|
+
|
|
8
|
+
module Olyx
|
|
9
|
+
module Guardrails
|
|
10
|
+
# Applies the explicit PII, secret, and restricted-policy transformation
|
|
11
|
+
# used by {Guardrails.redact}.
|
|
12
|
+
class Redactor
|
|
13
|
+
def self.call(input, policy:)
|
|
14
|
+
new(input, policy).call
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(input, policy)
|
|
18
|
+
@source = input.to_s
|
|
19
|
+
@policy = policy
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
Redaction::InputValidator.call(@source, @policy)
|
|
24
|
+
content = PolicyAwareRedactor.call(@source, policy: @policy)
|
|
25
|
+
Redaction::PublicResult.call(@source, content, @policy)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|