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,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'handler'
|
|
4
|
+
require_relative 'handler_collection_validator'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module Notification
|
|
9
|
+
# Validates and freezes named notification handlers.
|
|
10
|
+
module HandlerCollection
|
|
11
|
+
MAXIMUM = 20
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def call(handlers)
|
|
16
|
+
HandlerCollectionValidator.input!(handlers, maximum: MAXIMUM)
|
|
17
|
+
|
|
18
|
+
normalized = handlers.map { |name, handler| Handler.call(name, handler) }
|
|
19
|
+
HandlerCollectionValidator.unique!(normalized)
|
|
20
|
+
normalized.freeze
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Notification
|
|
6
|
+
# Validates handler collection shape and normalized-name uniqueness.
|
|
7
|
+
module HandlerCollectionValidator
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def input!(handlers, maximum:)
|
|
11
|
+
return if handlers.is_a?(Hash) && !handlers.empty? && handlers.length <= maximum
|
|
12
|
+
|
|
13
|
+
raise ArgumentError, "handlers must be a non-empty Hash with at most #{maximum} entries"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def unique!(handlers)
|
|
17
|
+
names = handlers.map(&:first)
|
|
18
|
+
raise ArgumentError, 'handler names must be unique' unless names.uniq.length == names.length
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Notification
|
|
6
|
+
# Normalizes a redacted metadata key.
|
|
7
|
+
module Key
|
|
8
|
+
LENGTH = 50
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(value, scrubber)
|
|
13
|
+
normalized = scrubber.call(value).gsub(/[^A-Za-z0-9_.-]/, '_')[0...LENGTH]
|
|
14
|
+
normalized.empty? ? 'metadata' : normalized
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Notification
|
|
6
|
+
# Bounds, sanitizes, and collision-proofs notification metadata.
|
|
7
|
+
module Metadata
|
|
8
|
+
MAX_ENTRIES = 20
|
|
9
|
+
KEY_LENGTH = 50
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(metadata, sanitizer)
|
|
14
|
+
metadata.first(MAX_ENTRIES).each_with_object({}) do |(key, value), output|
|
|
15
|
+
safe_key = unique_key(sanitizer.key(key), output)
|
|
16
|
+
output[safe_key] = sanitizer.field(value)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def unique_key(base, output)
|
|
21
|
+
index = 1
|
|
22
|
+
candidate = base
|
|
23
|
+
while output.key?(candidate)
|
|
24
|
+
index += 1
|
|
25
|
+
suffix = "_#{index}"
|
|
26
|
+
candidate = "#{base[0...(KEY_LENGTH - suffix.length)]}#{suffix}"
|
|
27
|
+
end
|
|
28
|
+
candidate
|
|
29
|
+
end
|
|
30
|
+
private_class_method :unique_key
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Notification
|
|
6
|
+
# Constructs the notifier's validated policy and delivery dispatcher.
|
|
7
|
+
module NotifierSetup
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(policy, handlers)
|
|
11
|
+
config = NotifierConfiguration.new(policy: policy, handlers: handlers)
|
|
12
|
+
configured_policy = config.policy
|
|
13
|
+
sanitizer = NotificationSanitizer.new(configured_policy)
|
|
14
|
+
[configured_policy, DeliveryDispatcher.new(config.handlers, sanitizer)]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Notification
|
|
6
|
+
# Produces a bounded redacted preview with a truncation marker.
|
|
7
|
+
module Preview
|
|
8
|
+
LENGTH = 300
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(value, scrubber)
|
|
13
|
+
source = value.to_s
|
|
14
|
+
scrubbed = scrubber.call(source)
|
|
15
|
+
preview = scrubbed[0...LENGTH]
|
|
16
|
+
preview += '…' if truncated?(source, scrubbed)
|
|
17
|
+
preview
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def truncated?(source, scrubbed)
|
|
21
|
+
scrubbed.length > LENGTH || source.length > Scrubber::WINDOW
|
|
22
|
+
end
|
|
23
|
+
private_class_method :truncated?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../policy_aware_redactor'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Notification
|
|
8
|
+
# Applies policy-aware redaction to a bounded notification value.
|
|
9
|
+
class Scrubber
|
|
10
|
+
WINDOW = 2_000
|
|
11
|
+
|
|
12
|
+
def initialize(policy)
|
|
13
|
+
@policy = policy
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(value)
|
|
17
|
+
PolicyAwareRedactor.call(value.to_s[0...WINDOW], policy: @policy)[:text]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../violation_labels'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Notification
|
|
8
|
+
# Derives stable machine-readable violation labels from a decision.
|
|
9
|
+
module ViolationLabels
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(result)
|
|
13
|
+
labels = Guardrails::ViolationLabels.call(result)
|
|
14
|
+
labels.empty? ? ['policy_violation'] : labels
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'notification/deep_freezer'
|
|
4
|
+
require_relative 'notification/base_event'
|
|
5
|
+
require_relative 'notification/metadata'
|
|
6
|
+
require_relative 'notification/violation_labels'
|
|
7
|
+
require_relative 'notification_sanitizer'
|
|
8
|
+
|
|
9
|
+
module Olyx
|
|
10
|
+
# Vendor-neutral AI safety checks and notification events.
|
|
11
|
+
module Guardrails
|
|
12
|
+
# Composes the bounded, immutable, vendor-neutral notification event.
|
|
13
|
+
class NotificationEventBuilder
|
|
14
|
+
SCHEMA_VERSION = 1
|
|
15
|
+
|
|
16
|
+
def self.call(result, input:, metadata:, policy:)
|
|
17
|
+
new(result, input, metadata, policy).call
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(result, input, metadata, policy)
|
|
21
|
+
@result = result
|
|
22
|
+
@input = input
|
|
23
|
+
@metadata = metadata.is_a?(Hash) ? metadata : {}
|
|
24
|
+
@policy = policy
|
|
25
|
+
@sanitizer = NotificationSanitizer.new(policy)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call
|
|
29
|
+
event = Notification::BaseEvent.call(
|
|
30
|
+
@result, metadata: @metadata, policy: @policy, sanitizer: @sanitizer, schema_version: SCHEMA_VERSION
|
|
31
|
+
)
|
|
32
|
+
reason = @result.dig(:ai_analysis, :reason)
|
|
33
|
+
event[:ai_reason] = @sanitizer.field(reason) if reason
|
|
34
|
+
event[:input_preview] = @sanitizer.preview(@input) unless @input.nil?
|
|
35
|
+
Notification::DeepFreezer.call(event)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private_constant :NotificationEventBuilder
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'notification/key'
|
|
4
|
+
require_relative 'notification/preview'
|
|
5
|
+
require_relative 'notification/scrubber'
|
|
6
|
+
|
|
7
|
+
module Olyx
|
|
8
|
+
# Guardrail policy types and evaluation services.
|
|
9
|
+
module Guardrails
|
|
10
|
+
# Applies the configured confidentiality policy to notification text.
|
|
11
|
+
class NotificationSanitizer
|
|
12
|
+
FIELD_LENGTH = 300
|
|
13
|
+
|
|
14
|
+
def initialize(policy)
|
|
15
|
+
@scrubber = Notification::Scrubber.new(policy)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def field(value, max_length: FIELD_LENGTH)
|
|
19
|
+
@scrubber.call(value).gsub(/[\r\n\t]+/, ' ')[0...max_length]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def preview(value)
|
|
23
|
+
Notification::Preview.call(value, @scrubber)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def key(value)
|
|
27
|
+
Notification::Key.call(value, @scrubber)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private_constant :NotificationSanitizer
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'notification_event_builder'
|
|
4
|
+
require_relative 'notification/delivery_dispatcher'
|
|
5
|
+
require_relative 'notification/delivery_summary'
|
|
6
|
+
require_relative 'notification/notifier_setup'
|
|
7
|
+
require_relative 'notification_sanitizer'
|
|
8
|
+
require_relative 'notifier_configuration'
|
|
9
|
+
|
|
10
|
+
module Olyx
|
|
11
|
+
# Guardrail policy types and evaluation services.
|
|
12
|
+
module Guardrails
|
|
13
|
+
# Dispatches one sanitized guardrail event to named application callables.
|
|
14
|
+
# Handlers run synchronously and independently; applications can enqueue
|
|
15
|
+
# asynchronous work from a handler when required.
|
|
16
|
+
class Notifier
|
|
17
|
+
# @param policy [Policy] the policy used to produce and sanitize results.
|
|
18
|
+
# @param handlers [Hash<String, #call>, Hash<Symbol, #call>] one to 20
|
|
19
|
+
# named callables accepting the immutable event Hash.
|
|
20
|
+
# @raise [ArgumentError] when policy or handler configuration is invalid.
|
|
21
|
+
def initialize(policy:, handlers:)
|
|
22
|
+
@policy, @dispatcher = Notification::NotifierSetup.call(policy, handlers)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Dispatches a sanitized event when the result has non-zero risk.
|
|
26
|
+
# Handler and event-building failures are returned and never raised.
|
|
27
|
+
#
|
|
28
|
+
# @param result [Hash] a result from {Guardrails.check}.
|
|
29
|
+
# @param input [#to_s, nil] original input for a redacted preview.
|
|
30
|
+
# @param metadata [Hash] caller context, capped and policy-redacted.
|
|
31
|
+
# @return [Hash, nil] delivery summary, or nil for zero-risk results.
|
|
32
|
+
def notify(result, input: nil, metadata: {})
|
|
33
|
+
return nil unless deliverable?(result)
|
|
34
|
+
|
|
35
|
+
event = build_event(result, input, metadata)
|
|
36
|
+
deliveries = @dispatcher.call(event)
|
|
37
|
+
Notification::DeliverySummary.call(event, deliveries)
|
|
38
|
+
rescue StandardError => error
|
|
39
|
+
@dispatcher.failure(error)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def build_event(result, input, metadata)
|
|
45
|
+
NotificationEventBuilder.call(result, input: input, metadata: metadata, policy: @policy)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def deliverable?(result)
|
|
49
|
+
result.is_a?(Hash) && result[:risk_score].to_f.positive?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'policy'
|
|
4
|
+
require_relative 'notification/handler_collection'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
# Guardrail policy types and evaluation services.
|
|
8
|
+
module Guardrails
|
|
9
|
+
# Validates and freezes the generic notifier's policy and named handlers.
|
|
10
|
+
class NotifierConfiguration
|
|
11
|
+
attr_reader :policy, :handlers
|
|
12
|
+
|
|
13
|
+
def initialize(policy:, handlers:)
|
|
14
|
+
@policy = validate_policy(policy)
|
|
15
|
+
@handlers = Notification::HandlerCollection.call(handlers)
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def validate_policy(policy)
|
|
22
|
+
return policy if policy.is_a?(Policy)
|
|
23
|
+
|
|
24
|
+
raise ArgumentError, 'policy must be an Olyx::Guardrails::Policy'
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private_constant :NotifierConfiguration
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Pii
|
|
8
|
+
# Accepts valid day-first or month-first numeric dates.
|
|
9
|
+
module AmbiguousNumericDate
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(parts)
|
|
13
|
+
first, second, raw_year = parts.map(&:to_i)
|
|
14
|
+
valid?(first, second, year(raw_year, parts.last))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def valid?(first, second, year)
|
|
18
|
+
candidates(first, second).any? { |month, day| Date.valid_date?(year, month, day) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def candidates(first, second)
|
|
22
|
+
[[second, first], [first, second]]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def year(value, source)
|
|
26
|
+
source.length == 2 ? value + 2_000 : value
|
|
27
|
+
end
|
|
28
|
+
private_class_method :candidates, :valid?, :year
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'hash_key'
|
|
4
|
+
require_relative 'text_scrubber'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module Pii
|
|
9
|
+
# Redacts a text content block while preserving its Hash shape.
|
|
10
|
+
module BlockScrubber
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(block)
|
|
14
|
+
return [block, false] unless block.is_a?(Hash)
|
|
15
|
+
|
|
16
|
+
key = HashKey.call(block, 'text')
|
|
17
|
+
text = block[key] if key
|
|
18
|
+
return [block, false] unless text.is_a?(String)
|
|
19
|
+
|
|
20
|
+
result(block, key, text, TextScrubber.call(text))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def result(block, key, text, redacted)
|
|
24
|
+
changed = redacted != text
|
|
25
|
+
[changed ? block.merge(key => redacted) : block, changed]
|
|
26
|
+
end
|
|
27
|
+
private_class_method :result
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'block_scrubber'
|
|
4
|
+
require_relative 'text_scrubber'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module Pii
|
|
9
|
+
# Redacts String or array-style message content.
|
|
10
|
+
module ContentScrubber
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(content)
|
|
14
|
+
return scrub_text(content) if content.is_a?(String)
|
|
15
|
+
return scrub_blocks(content) if content.is_a?(Array)
|
|
16
|
+
|
|
17
|
+
[content, false]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def scrub_text(text)
|
|
21
|
+
redacted = TextScrubber.call(text)
|
|
22
|
+
[redacted, redacted != text]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def scrub_blocks(blocks)
|
|
26
|
+
results = blocks.map { |block| BlockScrubber.call(block) }
|
|
27
|
+
[results.map(&:first), results.any?(&:last)]
|
|
28
|
+
end
|
|
29
|
+
private_class_method :scrub_text, :scrub_blocks
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require_relative 'named_date_validator'
|
|
5
|
+
require_relative 'numeric_date_validator'
|
|
6
|
+
|
|
7
|
+
module Olyx
|
|
8
|
+
module Guardrails
|
|
9
|
+
module Pii
|
|
10
|
+
# Validates labeled date-of-birth candidates as real calendar dates.
|
|
11
|
+
module DateValidator
|
|
12
|
+
DATE = %r{(?:\d{1,4}[/-]\d{1,2}[/-]\d{1,4}|[A-Za-z]+\.?\s+\d{1,2},?\s+\d{4})}
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def call(value)
|
|
17
|
+
candidate = value[DATE]
|
|
18
|
+
return false unless candidate
|
|
19
|
+
|
|
20
|
+
candidate.match?(/\A\d/) ? NumericDateValidator.call(candidate) : NamedDateValidator.call(candidate)
|
|
21
|
+
end
|
|
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 Pii
|
|
6
|
+
# Finds String or Symbol variants of a Hash key.
|
|
7
|
+
module HashKey
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(hash, name)
|
|
11
|
+
return name if hash.key?(name)
|
|
12
|
+
|
|
13
|
+
symbol = name.to_sym
|
|
14
|
+
symbol if hash.key?(symbol)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Pii
|
|
6
|
+
# Calculates ISO 13616's streaming mod-97 remainder.
|
|
7
|
+
module IbanRemainder
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(value)
|
|
11
|
+
value.each_char.reduce(0) { |current, character| append(current, digits(character)) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def append(current, characters)
|
|
15
|
+
characters.each_char.reduce(current) { |memo, digit| ((memo * 10) + digit.to_i) % 97 }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def digits(character)
|
|
19
|
+
character.match?(/[A-Z]/) ? (character.ord - 55).to_s : character
|
|
20
|
+
end
|
|
21
|
+
private_class_method :append, :digits
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'iban_remainder'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Pii
|
|
8
|
+
# Applies ISO 13616 mod-97 validation to IBAN candidates.
|
|
9
|
+
module IbanValidator
|
|
10
|
+
FORMAT = /\A[A-Z]{2}\d{2}[A-Z0-9]{11,30}\z/
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(iban)
|
|
15
|
+
compact = compact(iban)
|
|
16
|
+
return false unless valid_format?(compact)
|
|
17
|
+
|
|
18
|
+
valid_checksum?(compact)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def compact(iban) = iban.delete(' ').upcase
|
|
22
|
+
|
|
23
|
+
def valid_format?(compact)
|
|
24
|
+
compact.length.between?(15, 34) && compact.match?(FORMAT)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def valid_checksum?(compact)
|
|
28
|
+
IbanRemainder.call(compact[4..] + compact[0, 4]) == 1
|
|
29
|
+
end
|
|
30
|
+
private_class_method :compact, :valid_checksum?, :valid_format?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Pii
|
|
6
|
+
# Validates the structure and range of every IPv4 octet.
|
|
7
|
+
module Ipv4Validator
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(address)
|
|
11
|
+
octets = address.split('.')
|
|
12
|
+
octets.length == 4 && octets.all? { |octet| octet.to_i.between?(0, 255) }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Pii
|
|
8
|
+
# Validates IPv6 candidates with Ruby's address parser.
|
|
9
|
+
module Ipv6Validator
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(address)
|
|
13
|
+
IPAddr.new(address).ipv6?
|
|
14
|
+
rescue IPAddr::InvalidAddressError
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Pii
|
|
6
|
+
# Shared Luhn digit-parsing and checksum validity, reused by every
|
|
7
|
+
# format that applies the Luhn algorithm at its own fixed or minimum
|
|
8
|
+
# length (cards, Canadian SIN) so that length rule is the only thing
|
|
9
|
+
# each caller needs to own.
|
|
10
|
+
module LuhnChecksum
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def digits(number) = number.gsub(/\D/, '').chars.map(&:to_i)
|
|
14
|
+
|
|
15
|
+
def valid?(digits) = (checksum(digits) % 10).zero?
|
|
16
|
+
|
|
17
|
+
def checksum(digits)
|
|
18
|
+
digits.reverse.each_with_index.sum { |digit, index| index.even? ? digit : doubled(digit) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def doubled(digit)
|
|
22
|
+
value = digit * 2
|
|
23
|
+
value > 9 ? value - 9 : value
|
|
24
|
+
end
|
|
25
|
+
private_class_method :doubled
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'luhn_checksum'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Pii
|
|
8
|
+
# Validates card-like digit sequences with the Luhn checksum.
|
|
9
|
+
module LuhnValidator
|
|
10
|
+
MINIMUM_LENGTH = 13
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(number)
|
|
15
|
+
digits = LuhnChecksum.digits(number)
|
|
16
|
+
digits.length >= MINIMUM_LENGTH && LuhnChecksum.valid?(digits)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|