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,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyComponents
|
|
6
|
+
# Presents policy matches as a scan decision.
|
|
7
|
+
module ScanResult
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(findings)
|
|
11
|
+
violated = findings.any?
|
|
12
|
+
{
|
|
13
|
+
violated: violated,
|
|
14
|
+
blocked: violated && findings.any? { |finding| finding[:rule].block? },
|
|
15
|
+
findings: FindingPresenter.call(findings)
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../secret_scanner'
|
|
4
|
+
require_relative '../validation'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module PolicyComponents
|
|
9
|
+
# Validates custom secret patterns once during policy construction.
|
|
10
|
+
module SecretPatternCollection
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(values)
|
|
14
|
+
Validation.array_of!(values, String, name: 'policy secret_patterns')
|
|
15
|
+
patterns = values.map { |pattern| pattern.dup.freeze }.freeze
|
|
16
|
+
SecretScanner.scan('', custom_patterns: patterns)
|
|
17
|
+
patterns
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyComponents
|
|
6
|
+
# Accumulates transformed gaps and fixed policy replacements in source order.
|
|
7
|
+
class UnmatchedSegmentBuilder
|
|
8
|
+
def initialize(source, transform)
|
|
9
|
+
@source = source
|
|
10
|
+
@transform = transform
|
|
11
|
+
@cursor = 0
|
|
12
|
+
@parts = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def append(span)
|
|
16
|
+
@parts << @transform.call(@source[@cursor...span[:start]])
|
|
17
|
+
@parts << span[:replacement]
|
|
18
|
+
@cursor = span[:end]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def finish
|
|
22
|
+
@parts << @transform.call(@source[@cursor..])
|
|
23
|
+
@parts.join
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'unmatched_segment_builder'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module PolicyComponents
|
|
8
|
+
# Applies a secondary transformation only outside policy spans.
|
|
9
|
+
module UnmatchedTransformer
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(source, spans, transform)
|
|
13
|
+
builder = UnmatchedSegmentBuilder.new(source, transform)
|
|
14
|
+
spans.each { |span| builder.append(span) }
|
|
15
|
+
builder.finish
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'policy/configuration_hash'
|
|
4
|
+
require_relative 'policy/configuration'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
# Immutable enforcement configuration for decisions and transformations.
|
|
9
|
+
class Policy
|
|
10
|
+
def self.default
|
|
11
|
+
@default ||= new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_h(config)
|
|
15
|
+
new(**PolicyComponents::ConfigurationHash.call(config))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(
|
|
19
|
+
name: 'default',
|
|
20
|
+
max_input_length: 10_000,
|
|
21
|
+
block_pii: false,
|
|
22
|
+
block_injections: true,
|
|
23
|
+
block_secrets: false,
|
|
24
|
+
ai_failure_mode: :allow,
|
|
25
|
+
secret_patterns: [],
|
|
26
|
+
rules: []
|
|
27
|
+
)
|
|
28
|
+
@configuration = PolicyComponents::Configuration.new(
|
|
29
|
+
identity: [name, max_input_length],
|
|
30
|
+
blocking: [block_pii, block_injections, block_secrets],
|
|
31
|
+
restrictions: [ai_failure_mode, secret_patterns, rules]
|
|
32
|
+
)
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def name = @configuration.identity.name
|
|
37
|
+
def max_input_length = @configuration.identity.max_input_length
|
|
38
|
+
def ai_failure_mode = @configuration.restrictions.ai_failure_mode
|
|
39
|
+
def secret_patterns = @configuration.restrictions.secret_patterns
|
|
40
|
+
def rules = @configuration.restrictions.rules
|
|
41
|
+
def block_pii? = @configuration.blocking.pii?
|
|
42
|
+
def block_injections? = @configuration.blocking.injections?
|
|
43
|
+
def block_secrets? = @configuration.blocking.secrets?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pii_scrubber'
|
|
4
|
+
require_relative 'policy_scanner'
|
|
5
|
+
require_relative 'secret_scanner'
|
|
6
|
+
require_relative 'redaction/content_result'
|
|
7
|
+
require_relative 'redaction/unrestricted_text'
|
|
8
|
+
|
|
9
|
+
module Olyx
|
|
10
|
+
# Guardrail policy types and evaluation services.
|
|
11
|
+
module Guardrails
|
|
12
|
+
# Produces one policy-aware redaction result while preserving findings from
|
|
13
|
+
# the original source. Policy spans take precedence over narrower scanner
|
|
14
|
+
# substitutions; confidentiality markers remain fail-closed.
|
|
15
|
+
class PolicyAwareRedactor
|
|
16
|
+
def self.call(source, policy:)
|
|
17
|
+
new(source, policy).call
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(source, policy)
|
|
21
|
+
@source = source.to_s
|
|
22
|
+
@policy = policy
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call
|
|
26
|
+
secret_scan = SecretScanner.scan(
|
|
27
|
+
@source,
|
|
28
|
+
custom_patterns: @policy.secret_patterns
|
|
29
|
+
)
|
|
30
|
+
policy_redaction = PolicyScanner.redact(@source, policy: @policy) do |text|
|
|
31
|
+
Redaction::UnrestrictedText.call(text, @policy)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Redaction::ContentResult.call(
|
|
35
|
+
@source, text: policy_redaction[:text], secret_scan: secret_scan, policy_redaction: policy_redaction
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private_constant :PolicyAwareRedactor
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'policy/redaction_spans'
|
|
4
|
+
require_relative 'policy/unmatched_transformer'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
# Policy-driven safety checks and redaction for Ruby applications.
|
|
8
|
+
module Guardrails
|
|
9
|
+
# Applies offset-aware replacements for private policy findings.
|
|
10
|
+
class PolicyRedactor
|
|
11
|
+
def self.call(source, findings, transform: nil)
|
|
12
|
+
spans = PolicyComponents::RedactionSpans.call(findings)
|
|
13
|
+
return PolicyComponents::UnmatchedTransformer.call(source, spans, transform) if transform
|
|
14
|
+
|
|
15
|
+
replace(source, spans)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.replace(source, spans)
|
|
19
|
+
spans.reverse_each.with_object(source.dup) do |span, output|
|
|
20
|
+
output[span[:start]...span[:end]] = span[:replacement]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
private_class_method :replace
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private_constant :PolicyRedactor
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'match_mode'
|
|
4
|
+
require_relative 'pattern_compiler'
|
|
5
|
+
require_relative 'values'
|
|
6
|
+
|
|
7
|
+
module Olyx
|
|
8
|
+
module Guardrails
|
|
9
|
+
module PolicyRuleComponents
|
|
10
|
+
# Validated identity and descriptive policy-rule fields.
|
|
11
|
+
class IdentityConfiguration
|
|
12
|
+
attr_reader :name, :description
|
|
13
|
+
|
|
14
|
+
def initialize(name, description)
|
|
15
|
+
@name = Values.name(name)
|
|
16
|
+
@description = Values.description(description)
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Validated policy-rule matching configuration.
|
|
22
|
+
class MatchConfiguration
|
|
23
|
+
attr_reader :mode, :patterns
|
|
24
|
+
|
|
25
|
+
def initialize(patterns, terms, match)
|
|
26
|
+
@mode = MatchMode.call(match)
|
|
27
|
+
@patterns = PatternCompiler.call(patterns: patterns, terms: terms, match: @mode)
|
|
28
|
+
freeze
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Validated policy-rule enforcement behavior.
|
|
33
|
+
class EnforcementConfiguration
|
|
34
|
+
attr_reader :replacement
|
|
35
|
+
|
|
36
|
+
def initialize(block, replacement)
|
|
37
|
+
@block = Values.boolean(block)
|
|
38
|
+
@replacement = Values.replacement(replacement)
|
|
39
|
+
freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def block? = @block
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyRuleComponents
|
|
6
|
+
# Validates an optional human-readable rule description.
|
|
7
|
+
module DescriptionValue
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(value)
|
|
11
|
+
return nil if value.nil?
|
|
12
|
+
return value.dup.freeze if valid?(value)
|
|
13
|
+
|
|
14
|
+
raise ArgumentError, 'policy rule description must be a String of 1..500 characters or nil'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def valid?(value)
|
|
18
|
+
value.is_a?(String) && !value.strip.empty? && value.length <= 500
|
|
19
|
+
end
|
|
20
|
+
private_class_method :valid?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
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 PolicyRuleComponents
|
|
8
|
+
# Validates how configured terms are interpreted.
|
|
9
|
+
MatchMode = EnumValue.new(
|
|
10
|
+
allowed: %i[substring whole_word regexp],
|
|
11
|
+
error: 'policy rule match must be substring, whole_word, or regexp'
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyRuleComponents
|
|
6
|
+
# Validates and freezes a policy-rule identifier.
|
|
7
|
+
module NameValue
|
|
8
|
+
FORMAT = /\A[a-z][a-z0-9_.:-]*\z/i
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(value)
|
|
13
|
+
normalized = value.to_s
|
|
14
|
+
valid = (value.is_a?(String) || value.is_a?(Symbol)) && normalized.match?(FORMAT)
|
|
15
|
+
raise ArgumentError, 'policy rule name must be a String or Symbol identifier' unless valid
|
|
16
|
+
|
|
17
|
+
normalized.dup.freeze
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'regexp_compiler'
|
|
4
|
+
require_relative 'term_compiler'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module PolicyRuleComponents
|
|
9
|
+
# Compiles bounded policy regexes and rejects empty matching patterns.
|
|
10
|
+
module PatternCompiler
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(patterns:, terms:, match:)
|
|
14
|
+
validate_collections!(patterns, terms)
|
|
15
|
+
compiled = patterns.map { |value| RegexpCompiler.call(value) }
|
|
16
|
+
compiled.concat(terms.map { |term| TermCompiler.call(term, mode: match) }).freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate_collections!(patterns, terms)
|
|
20
|
+
valid = patterns.is_a?(Array) && terms.is_a?(Array) && !(patterns.empty? && terms.empty?)
|
|
21
|
+
raise ArgumentError, 'policy rule patterns or terms must be a non-empty Array' unless valid
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private_class_method :validate_collections!
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyRuleComponents
|
|
6
|
+
# Compiles bounded regular-expression policy patterns.
|
|
7
|
+
module RegexpCompiler
|
|
8
|
+
TIMEOUT = 0.1
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(value)
|
|
13
|
+
pattern = build(value)
|
|
14
|
+
raise ArgumentError, 'policy rule patterns must not match empty text' if pattern.match?('')
|
|
15
|
+
|
|
16
|
+
pattern
|
|
17
|
+
rescue RegexpError => error
|
|
18
|
+
raise ArgumentError, "invalid policy rule pattern #{value.inspect}: #{error.message}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build(value)
|
|
22
|
+
return Regexp.new(value, Regexp::IGNORECASE, timeout: TIMEOUT) if value.is_a?(String)
|
|
23
|
+
return Regexp.new(value.source, value.options, timeout: TIMEOUT) if value.is_a?(Regexp)
|
|
24
|
+
|
|
25
|
+
raise ArgumentError, 'policy rule patterns must contain only Strings or Regexps'
|
|
26
|
+
end
|
|
27
|
+
private_class_method :build
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module PolicyRuleComponents
|
|
6
|
+
# Validates a bounded single-line rule replacement.
|
|
7
|
+
module ReplacementValue
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(value)
|
|
11
|
+
return value.dup.freeze if valid?(value)
|
|
12
|
+
|
|
13
|
+
raise ArgumentError, 'policy rule replacement must be a single-line String of 1..100 characters'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def valid?(value)
|
|
17
|
+
value.is_a?(String) && bounded?(value) && single_line?(value)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def bounded?(value) = !value.empty? && value.length <= 100
|
|
21
|
+
def single_line?(value) = !value.match?(/[\r\n\t]/)
|
|
22
|
+
private_class_method :bounded?, :single_line?, :valid?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'regexp_compiler'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module PolicyRuleComponents
|
|
8
|
+
# Compiles literal terms without exposing regex semantics.
|
|
9
|
+
module TermCompiler
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(term, mode:)
|
|
13
|
+
validate!(term)
|
|
14
|
+
return RegexpCompiler.call(term) if mode == :regexp
|
|
15
|
+
|
|
16
|
+
Regexp.new(source(term, mode), Regexp::IGNORECASE, timeout: RegexpCompiler::TIMEOUT)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def source(term, mode)
|
|
20
|
+
escaped = Regexp.escape(term)
|
|
21
|
+
mode == :whole_word ? "(?<![[:alnum:]_])#{escaped}(?![[:alnum:]_])" : escaped
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate!(term)
|
|
25
|
+
return if term.is_a?(String) && !term.empty?
|
|
26
|
+
|
|
27
|
+
raise ArgumentError, 'policy rule terms must contain only non-empty Strings'
|
|
28
|
+
end
|
|
29
|
+
private_class_method :source, :validate!
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'description_value'
|
|
4
|
+
require_relative 'replacement_value'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module PolicyRuleComponents
|
|
9
|
+
# Validates human-readable rule description and replacement values.
|
|
10
|
+
module TextValues
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def description(value)
|
|
14
|
+
DescriptionValue.call(value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def replacement(value)
|
|
18
|
+
ReplacementValue.call(value)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'name_value'
|
|
4
|
+
require_relative 'text_values'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module PolicyRuleComponents
|
|
9
|
+
# Validates non-pattern policy-rule values.
|
|
10
|
+
module Values
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def name(value)
|
|
14
|
+
NameValue.call(value)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def boolean(value)
|
|
18
|
+
return value if [true, false].include?(value)
|
|
19
|
+
|
|
20
|
+
raise ArgumentError, 'policy rule block must be true or false'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def description(value)
|
|
24
|
+
TextValues.description(value)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def replacement(value)
|
|
28
|
+
TextValues.replacement(value)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'policy_rule/configuration'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
# Immutable named restricted-content rule.
|
|
8
|
+
class PolicyRule
|
|
9
|
+
def initialize(name:, patterns: [], terms: [], match: :substring, block: true, description: nil, replacement: nil)
|
|
10
|
+
@identity = PolicyRuleComponents::IdentityConfiguration.new(name, description)
|
|
11
|
+
@matching = PolicyRuleComponents::MatchConfiguration.new(patterns, terms, match)
|
|
12
|
+
@enforcement = PolicyRuleComponents::EnforcementConfiguration.new(block, replacement || default_replacement)
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def name = @identity.name
|
|
17
|
+
def description = @identity.description
|
|
18
|
+
def match = @matching.mode
|
|
19
|
+
def patterns = @matching.patterns
|
|
20
|
+
def replacement = @enforcement.replacement
|
|
21
|
+
def block? = @enforcement.block?
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def default_replacement
|
|
26
|
+
"[RESTRICTED:#{@identity.name.upcase}]"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'policy/finding_presenter'
|
|
4
|
+
require_relative 'policy/match_collector'
|
|
5
|
+
require_relative 'policy/redaction_result'
|
|
6
|
+
require_relative 'policy/scan_result'
|
|
7
|
+
require_relative 'policy_redactor'
|
|
8
|
+
|
|
9
|
+
module Olyx
|
|
10
|
+
# Policy-driven safety checks and redaction for Ruby applications.
|
|
11
|
+
module Guardrails
|
|
12
|
+
# Presents restricted-content decisions while keeping matched text private.
|
|
13
|
+
class PolicyScanner
|
|
14
|
+
def self.scan(text, policy:)
|
|
15
|
+
new(text, policy).scan
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.redact(text, policy:, &transform)
|
|
19
|
+
new(text, policy).redact(&transform)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(text, policy)
|
|
23
|
+
@source = text.to_s
|
|
24
|
+
@policy = policy
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def scan
|
|
28
|
+
PolicyComponents::ScanResult.call(raw_findings)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def redact(&transform)
|
|
32
|
+
PolicyComponents::RedactionResult.call(@source, raw_findings, transform)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def raw_findings
|
|
38
|
+
PolicyComponents::MatchCollector.call(@source, @policy.rules)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private_constant :PolicyScanner
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'ingress'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Rails
|
|
8
|
+
# Opt-in enforcement helpers for Action Cable channel payloads.
|
|
9
|
+
module ActionCable
|
|
10
|
+
include Ingress
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
alias guardrails_check_cable! guardrails_check_ingress!
|
|
15
|
+
alias guardrails_check_cable_output! guardrails_check_ingress_output!
|
|
16
|
+
|
|
17
|
+
def guardrails_ingress_key = :channel
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'job_reference'
|
|
4
|
+
require_relative 'queue_name'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module Rails
|
|
9
|
+
# Adapts immutable notification events to any Active Job-compatible job.
|
|
10
|
+
class ActiveJobHandler
|
|
11
|
+
def initialize(job:, queue: nil)
|
|
12
|
+
@job = JobReference.new(job)
|
|
13
|
+
@queue = QueueName.call(queue)
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(event)
|
|
18
|
+
raise ArgumentError, 'Active Job notification event must be a Hash' unless event.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
target = @job.resolve
|
|
21
|
+
target = target.set(queue: @queue) if @queue
|
|
22
|
+
target.perform_later(event)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_model'
|
|
4
|
+
|
|
5
|
+
# Explicit Active Model validator for AI-bound text attributes.
|
|
6
|
+
class OlyxGuardrailsValidator < ActiveModel::EachValidator
|
|
7
|
+
def validate_each(record, attribute, value)
|
|
8
|
+
result = evaluate(record, attribute, value)
|
|
9
|
+
return if result[:allowed]
|
|
10
|
+
|
|
11
|
+
record.errors.add(attribute, options.fetch(:message, 'was rejected by guardrail policy'))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def evaluate(record, attribute, value)
|
|
17
|
+
policy = options[:policy]
|
|
18
|
+
return Olyx::Guardrails.check(value, policy: policy) if policy
|
|
19
|
+
|
|
20
|
+
metadata = { model: record.class.name, attribute: attribute }
|
|
21
|
+
Olyx::Guardrails::Rails.check(value, metadata: metadata)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'instrumentation'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Rails
|
|
8
|
+
# Publishes check and violation telemetry from one decision summary.
|
|
9
|
+
module CheckTelemetry
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(result, duration)
|
|
13
|
+
Instrumentation.publish_check(result, duration)
|
|
14
|
+
Instrumentation.publish_violation(result, duration) if result[:risk_score].to_f.positive?
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|