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,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../validation'
|
|
4
|
+
require_relative 'content_scrubber'
|
|
5
|
+
require_relative 'hash_key'
|
|
6
|
+
|
|
7
|
+
module Olyx
|
|
8
|
+
module Guardrails
|
|
9
|
+
module Pii
|
|
10
|
+
# Preserves chat message shape while redacting String content fields.
|
|
11
|
+
module MessageScrubber
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(messages)
|
|
15
|
+
Validation.array_of!(messages, Hash, name: 'messages')
|
|
16
|
+
results = messages.map { |message| scrub_message(message) }
|
|
17
|
+
{ messages: results.map(&:first), detected: results.any?(&:last) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def scrub_message(message)
|
|
21
|
+
key = HashKey.call(message, 'content')
|
|
22
|
+
return [message, false] unless key
|
|
23
|
+
|
|
24
|
+
redacted, changed = ContentScrubber.call(message[key])
|
|
25
|
+
[changed ? message.merge(key => redacted) : message, changed]
|
|
26
|
+
end
|
|
27
|
+
private_class_method :scrub_message
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Pii
|
|
8
|
+
# Validates English abbreviated or full month dates.
|
|
9
|
+
module NamedDateValidator
|
|
10
|
+
FORMATS = ['%B %d, %Y', '%B %d %Y', '%b %d, %Y', '%b %d %Y'].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(value)
|
|
15
|
+
FORMATS.any? { |format| valid?(value.delete('.'), format) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def valid?(value, format)
|
|
19
|
+
Date.strptime(value, format)
|
|
20
|
+
true
|
|
21
|
+
rescue Date::Error
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
private_class_method :valid?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'ambiguous_numeric_date'
|
|
4
|
+
require_relative 'year_first_date'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module Pii
|
|
9
|
+
# Validates year-first or ambiguous day/month numeric dates.
|
|
10
|
+
module NumericDateValidator
|
|
11
|
+
PARTS = %r{\A(\d{1,4})[/-](\d{1,2})[/-](\d{1,4})\z}
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def call(value)
|
|
16
|
+
parts = value.match(PARTS)&.captures
|
|
17
|
+
return false unless parts
|
|
18
|
+
|
|
19
|
+
return YearFirstDate.call(parts) if parts.first.length == 4
|
|
20
|
+
|
|
21
|
+
AmbiguousNumericDate.call(parts)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../pii_validators'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Pii
|
|
8
|
+
# Declarative PII pattern, replacement, and validator catalog.
|
|
9
|
+
module PatternCatalog
|
|
10
|
+
EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/
|
|
11
|
+
PHONE = /(?<!\d)(?:(?:\+|00)\d(?:[\s\-.]?\d){7,15}|\(?\d{3}\)?[\s\-.]\d{3}[\s\-.]\d{4})(?!\d)/
|
|
12
|
+
SSN = /\b\d{3}[- ]\d{2}[- ]\d{4}\b/
|
|
13
|
+
# Canadian Social Insurance Number: 9 digits, Luhn-checksummed like a
|
|
14
|
+
# card number, so (unlike SSN) it's not gated on a keyword — the
|
|
15
|
+
# checksum alone keeps false positives in line with CARD below.
|
|
16
|
+
SIN = /\b\d{3}[\s-]?\d{3}[\s-]?\d{3}\b/
|
|
17
|
+
CARD = /\b\d(?:[ -]?\d){12,18}\b/
|
|
18
|
+
IPV4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/
|
|
19
|
+
IPV6 = /(?<![A-Fa-f0-9:])(?:[A-Fa-f0-9]{0,4}:){2,7}[A-Fa-f0-9]{0,4}(?![A-Fa-f0-9:])/
|
|
20
|
+
TOKEN = /\b(?:Bearer\s+|sk-|ak_live_|fy-ent-)[A-Za-z0-9._-]{8,}\b/i
|
|
21
|
+
PASSPORT = /\b(?:passport(?:\s+(?:no|number|#))?[\s:]+)([A-Z]{1,2}\d{6,9})\b/i
|
|
22
|
+
IBAN = /\b(?:[A-Z]{2}\d{2}[A-Z0-9]{11,30}|[A-Z]{2}\d{2}(?: [A-Z0-9]{4}){2,7}(?: [A-Z0-9]{1,3})?)\b/
|
|
23
|
+
DOB = %r{\b(?:dob|date\s+of\s+birth|born\s+on|birthday)[\s:]+(?:\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}[/-]\d{1,2}[/-]\d{1,2}|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},?\s+\d{4})}ix
|
|
24
|
+
|
|
25
|
+
ENTRIES = [
|
|
26
|
+
[EMAIL, '[EMAIL]'],
|
|
27
|
+
[SSN, '[SSN]', ->(value) { PiiValidators.ssn_valid?(value) }],
|
|
28
|
+
[SIN, '[SIN]', ->(value) { PiiValidators.sin_valid?(value) }],
|
|
29
|
+
[PASSPORT, '[PASSPORT]'],
|
|
30
|
+
[IBAN, '[IBAN]', ->(value) { PiiValidators.iban_valid?(value) }],
|
|
31
|
+
[DOB, '[DOB]', ->(value) { PiiValidators.date_valid?(value) }],
|
|
32
|
+
[IPV4, '[IP]', ->(value) { PiiValidators.ipv4_valid?(value) }],
|
|
33
|
+
[IPV6, '[IP]', ->(value) { PiiValidators.ipv6_valid?(value) }],
|
|
34
|
+
[TOKEN, '[TOKEN]'],
|
|
35
|
+
[CARD, '[CARD]', ->(value) { PiiValidators.luhn_valid?(value) }],
|
|
36
|
+
[PHONE, '[PHONE]']
|
|
37
|
+
].freeze
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'luhn_checksum'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Pii
|
|
8
|
+
# Validates Canadian Social Insurance Numbers with the same Luhn
|
|
9
|
+
# checksum as card numbers, but at SIN's fixed 9-digit length —
|
|
10
|
+
# {LuhnValidator} enforces its own 13-digit floor for card use, so this
|
|
11
|
+
# applies SIN's length rule against the shared {LuhnChecksum} directly.
|
|
12
|
+
module SinValidator
|
|
13
|
+
LENGTH = 9
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def call(sin)
|
|
18
|
+
digits = LuhnChecksum.digits(sin)
|
|
19
|
+
digits.length == LENGTH && LuhnChecksum.valid?(digits)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Pii
|
|
6
|
+
# Rejects structurally impossible U.S. Social Security numbers.
|
|
7
|
+
module SsnValidator
|
|
8
|
+
INVALID_AREA = /\A(?:000|666|9\d{2})\z/
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(ssn)
|
|
13
|
+
area, group, serial = ssn.split(/[- ]/)
|
|
14
|
+
return false unless area && group && serial
|
|
15
|
+
|
|
16
|
+
!area.match?(INVALID_AREA) && group != '00' && serial != '0000'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pattern_catalog'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Pii
|
|
8
|
+
# Redacts recognized PII from one String.
|
|
9
|
+
module TextScrubber
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(text)
|
|
13
|
+
return text unless text.is_a?(String)
|
|
14
|
+
|
|
15
|
+
PatternCatalog::ENTRIES.reduce(text) { |output, entry| replace(output, entry) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def replace(text, entry)
|
|
19
|
+
pattern, replacement, validator = entry
|
|
20
|
+
return text.gsub(pattern, replacement) unless validator
|
|
21
|
+
|
|
22
|
+
text.gsub(pattern) { |match| validator.call(match) ? replacement : match }
|
|
23
|
+
end
|
|
24
|
+
private_class_method :replace
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Pii
|
|
8
|
+
# Validates numeric dates whose first component is a four-digit year.
|
|
9
|
+
module YearFirstDate
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(parts)
|
|
13
|
+
Date.valid_date?(*parts.map(&:to_i))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pii/message_scrubber'
|
|
4
|
+
require_relative 'pii/text_scrubber'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
# Stable facade for free-text and chat-message PII redaction.
|
|
9
|
+
class PiiScrubber
|
|
10
|
+
def self.scrub(text)
|
|
11
|
+
Pii::TextScrubber.call(text)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.scrub_messages(messages)
|
|
15
|
+
scrub_messages_with_detection(messages)[:messages]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.scrub_messages_with_detection(messages)
|
|
19
|
+
Pii::MessageScrubber.call(messages)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pii/iban_validator'
|
|
4
|
+
require_relative 'pii/date_validator'
|
|
5
|
+
require_relative 'pii/ipv4_validator'
|
|
6
|
+
require_relative 'pii/ipv6_validator'
|
|
7
|
+
require_relative 'pii/luhn_validator'
|
|
8
|
+
require_relative 'pii/ssn_validator'
|
|
9
|
+
require_relative 'pii/sin_validator'
|
|
10
|
+
|
|
11
|
+
module Olyx
|
|
12
|
+
module Guardrails
|
|
13
|
+
# Stable facade over format-specific PII validators.
|
|
14
|
+
module PiiValidators
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def luhn_valid?(value) = Pii::LuhnValidator.call(value)
|
|
18
|
+
def ssn_valid?(value) = Pii::SsnValidator.call(value)
|
|
19
|
+
def ipv4_valid?(value) = Pii::Ipv4Validator.call(value)
|
|
20
|
+
def ipv6_valid?(value) = Pii::Ipv6Validator.call(value)
|
|
21
|
+
def iban_valid?(value) = Pii::IbanValidator.call(value)
|
|
22
|
+
def date_valid?(value) = Pii::DateValidator.call(value)
|
|
23
|
+
def sin_valid?(value) = Pii::SinValidator.call(value)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../enum_value'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module PolicyComponents
|
|
8
|
+
# Validates analyzer failure behavior.
|
|
9
|
+
AiFailureMode = EnumValue.new(
|
|
10
|
+
allowed: %i[allow block raise],
|
|
11
|
+
error: 'policy ai_failure_mode must be allow, block, or raise'
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../validation'
|
|
4
|
+
require_relative 'ai_failure_mode'
|
|
5
|
+
require_relative 'name'
|
|
6
|
+
require_relative 'rule_collection'
|
|
7
|
+
require_relative 'secret_pattern_collection'
|
|
8
|
+
|
|
9
|
+
module Olyx
|
|
10
|
+
module Guardrails
|
|
11
|
+
module PolicyComponents
|
|
12
|
+
# Validated policy identity and bounded input limit.
|
|
13
|
+
class IdentityConfiguration
|
|
14
|
+
attr_reader :name, :max_input_length
|
|
15
|
+
|
|
16
|
+
def initialize(name, max_input_length)
|
|
17
|
+
@name = Name.call(name)
|
|
18
|
+
@max_input_length = Validation.non_negative_integer!(max_input_length, name: 'policy max_input_length')
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Validated built-in blocking switches.
|
|
24
|
+
class BlockingConfiguration
|
|
25
|
+
def initialize(pii, injections, secrets)
|
|
26
|
+
@pii = Validation.boolean!(pii, name: 'policy block_pii')
|
|
27
|
+
@injections = Validation.boolean!(injections, name: 'policy block_injections')
|
|
28
|
+
@secrets = Validation.boolean!(secrets, name: 'policy block_secrets')
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def pii? = @pii
|
|
33
|
+
def injections? = @injections
|
|
34
|
+
def secrets? = @secrets
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Validated analyzer, secret, and restricted-content configuration.
|
|
38
|
+
class RestrictionConfiguration
|
|
39
|
+
attr_reader :ai_failure_mode, :secret_patterns, :rules
|
|
40
|
+
|
|
41
|
+
def initialize(ai_failure_mode, secret_patterns, rules)
|
|
42
|
+
@ai_failure_mode = AiFailureMode.call(ai_failure_mode)
|
|
43
|
+
@secret_patterns = SecretPatternCollection.call(secret_patterns)
|
|
44
|
+
@rules = RuleCollection.call(rules)
|
|
45
|
+
freeze
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Composes independent immutable policy configuration sections.
|
|
50
|
+
class Configuration
|
|
51
|
+
attr_reader :identity, :blocking, :restrictions
|
|
52
|
+
|
|
53
|
+
def initialize(identity:, blocking:, restrictions:)
|
|
54
|
+
@identity = IdentityConfiguration.new(*identity)
|
|
55
|
+
@blocking = BlockingConfiguration.new(*blocking)
|
|
56
|
+
@restrictions = RestrictionConfiguration.new(*restrictions)
|
|
57
|
+
freeze
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyComponents
|
|
6
|
+
# Normalizes String/Symbol keyed policy configuration hashes.
|
|
7
|
+
module ConfigurationHash
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(config)
|
|
11
|
+
raise ArgumentError, 'policy configuration must be a Hash' unless config.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
config.to_h { |key, value| [key.to_sym, value] }
|
|
14
|
+
rescue NoMethodError
|
|
15
|
+
raise ArgumentError, 'policy configuration keys must be Strings or Symbols'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyComponents
|
|
6
|
+
# Defines policy finding identity and deterministic ordering.
|
|
7
|
+
module FindingOrder
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def identity(finding)
|
|
11
|
+
[finding[:rule].name, finding[:start], finding[:end]]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def key(finding)
|
|
15
|
+
[finding[:start], finding[:end], finding[:rule_index]]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module PolicyComponents
|
|
8
|
+
# Converts private policy matches to masked public findings.
|
|
9
|
+
module FindingPresenter
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(findings)
|
|
13
|
+
findings.map { |finding| present(finding) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def present(finding)
|
|
17
|
+
rule = finding[:rule]
|
|
18
|
+
identity(rule, finding).merge(location(finding)).compact
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def identity(rule, finding)
|
|
22
|
+
{
|
|
23
|
+
rule: rule.name,
|
|
24
|
+
description: rule.description,
|
|
25
|
+
blocked: rule.block?,
|
|
26
|
+
matched: '[REDACTED]',
|
|
27
|
+
fingerprint: "sha256:#{Digest::SHA256.hexdigest(finding[:full])[0, 12]}"
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def location(finding)
|
|
32
|
+
{
|
|
33
|
+
start: finding[:start],
|
|
34
|
+
end: finding[:end]
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
private_class_method :identity, :location, :present
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'finding_order'
|
|
4
|
+
require_relative 'rule_matcher'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module PolicyComponents
|
|
9
|
+
# Collects, deduplicates, and orders private policy matches.
|
|
10
|
+
module MatchCollector
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(source, rules)
|
|
14
|
+
findings = rules.each_with_index.flat_map { |rule, index| RuleMatcher.call(source, rule, index) }
|
|
15
|
+
findings.uniq { |finding| FindingOrder.identity(finding) }.sort_by { |finding| FindingOrder.key(finding) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyComponents
|
|
6
|
+
# Validates and freezes an operator-facing policy identifier.
|
|
7
|
+
module Name
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(value)
|
|
11
|
+
return value.dup.freeze if valid?(value)
|
|
12
|
+
|
|
13
|
+
raise ArgumentError, 'policy name must be a String of 1..100 characters'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def valid?(value)
|
|
17
|
+
value.is_a?(String) && !value.strip.empty? && value.length <= 100
|
|
18
|
+
end
|
|
19
|
+
private_class_method :valid?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../text/mapped_normalization'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module PolicyComponents
|
|
8
|
+
# Finds normalized policy matches and maps them to original offsets.
|
|
9
|
+
module NormalizedPatternMatcher
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(source, rule, index, pattern)
|
|
13
|
+
mapped = Text::MappedNormalization.new(source)
|
|
14
|
+
return [] unless mapped.changed?
|
|
15
|
+
|
|
16
|
+
findings(source, mapped, rule, index, pattern)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def findings(source, mapped, rule, index, pattern)
|
|
20
|
+
mapped.text.to_enum(:scan, pattern).map do
|
|
21
|
+
match = Regexp.last_match
|
|
22
|
+
finding(source, mapped, rule, index, match)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def finding(source, mapped, rule, index, match)
|
|
27
|
+
starting, ending = mapped.original_span(match.begin(0), match.end(0))
|
|
28
|
+
{ rule: rule, rule_index: index, full: source[starting...ending], start: starting, end: ending }
|
|
29
|
+
end
|
|
30
|
+
private_class_method :finding, :findings
|
|
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 PolicyComponents
|
|
6
|
+
# Enumerates private offsets for one compiled policy pattern.
|
|
7
|
+
module PatternMatcher
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(source, rule, index, pattern)
|
|
11
|
+
source.to_enum(:scan, pattern).map do
|
|
12
|
+
match = Regexp.last_match
|
|
13
|
+
{ rule: rule, rule_index: index, full: match[0].to_s, start: match.begin(0), end: match.end(0) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyComponents
|
|
6
|
+
# Presents policy matches and their redacted output.
|
|
7
|
+
module RedactionResult
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(source, findings, transform)
|
|
11
|
+
{
|
|
12
|
+
text: PolicyRedactor.call(source, findings, transform: transform),
|
|
13
|
+
violated: findings.any?,
|
|
14
|
+
findings: FindingPresenter.call(findings)
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyComponents
|
|
6
|
+
# Merges overlapping policy findings while preserving the first rule.
|
|
7
|
+
module RedactionSpans
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(findings)
|
|
11
|
+
findings.each_with_object([]) { |finding, spans| append(spans, finding) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def append(spans, finding)
|
|
15
|
+
previous = spans.last
|
|
16
|
+
return spans << span(finding) unless overlaps?(previous, finding)
|
|
17
|
+
|
|
18
|
+
previous[:end] = [previous[:end], finding[:end]].max
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def overlaps?(previous, finding)
|
|
22
|
+
previous && finding[:start] < previous[:end]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def span(finding)
|
|
26
|
+
{ start: finding[:start], end: finding[:end], replacement: finding[:rule].replacement }
|
|
27
|
+
end
|
|
28
|
+
private_class_method :append, :overlaps?, :span
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../policy_rule'
|
|
4
|
+
require_relative 'configuration_hash'
|
|
5
|
+
require_relative 'rule_normalizer'
|
|
6
|
+
|
|
7
|
+
module Olyx
|
|
8
|
+
module Guardrails
|
|
9
|
+
module PolicyComponents
|
|
10
|
+
# Normalizes policy rules and enforces unique identifiers.
|
|
11
|
+
module RuleCollection
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(values)
|
|
15
|
+
validate_collection!(values)
|
|
16
|
+
|
|
17
|
+
rules = values.map { |value| RuleNormalizer.call(value) }
|
|
18
|
+
validate_unique_names!(rules)
|
|
19
|
+
rules.freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate_collection!(values)
|
|
23
|
+
raise ArgumentError, RuleNormalizer::ERROR unless values.is_a?(Array)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validate_unique_names!(rules)
|
|
27
|
+
names = rules.map(&:name)
|
|
28
|
+
raise ArgumentError, 'policy rule names must be unique' unless names.uniq.length == names.length
|
|
29
|
+
end
|
|
30
|
+
private_class_method :validate_collection!, :validate_unique_names!
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pattern_matcher'
|
|
4
|
+
require_relative 'normalized_pattern_matcher'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module PolicyComponents
|
|
9
|
+
# Collects original and normalized matches for one policy rule.
|
|
10
|
+
module RuleMatcher
|
|
11
|
+
TIMEOUT_ERROR = defined?(Regexp::TimeoutError) ? Regexp::TimeoutError : RegexpError
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def call(source, rule, index)
|
|
16
|
+
rule.patterns.flat_map { |pattern| matches(source, rule, index, pattern) }
|
|
17
|
+
rescue TIMEOUT_ERROR
|
|
18
|
+
raise ArgumentError, "policy rule #{rule.name.inspect} timed out"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def matches(source, rule, index, pattern)
|
|
22
|
+
PatternMatcher.call(source, rule, index, pattern) +
|
|
23
|
+
NormalizedPatternMatcher.call(source, rule, index, pattern)
|
|
24
|
+
end
|
|
25
|
+
private_class_method :matches
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyComponents
|
|
6
|
+
# Converts policy rule Hashes into immutable PolicyRule values.
|
|
7
|
+
module RuleNormalizer
|
|
8
|
+
ERROR = 'policy rules must be an Array of PolicyRule or Hash values'
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(value)
|
|
13
|
+
return value if value.is_a?(PolicyRule)
|
|
14
|
+
return PolicyRule.new(**ConfigurationHash.call(value)) if value.is_a?(Hash)
|
|
15
|
+
|
|
16
|
+
raise ArgumentError, ERROR
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|