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,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Risk
|
|
6
|
+
# Coerces an optional analyzer score into the supported range.
|
|
7
|
+
module AiScore
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(result)
|
|
11
|
+
return unless result
|
|
12
|
+
|
|
13
|
+
score = Float(result[:risk_score], exception: false)
|
|
14
|
+
score.clamp(0.0, 1.0) if score&.finite?
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'weights'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Risk
|
|
8
|
+
# Sums risk weights for positive deterministic findings.
|
|
9
|
+
module CheckWeights
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(checks)
|
|
13
|
+
Weights::FINDINGS.sum { |check, (finding, weight)| checks[check][finding] ? weight : 0.0 }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'check_weights'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Risk
|
|
8
|
+
# Computes the bounded score contributed by deterministic checks.
|
|
9
|
+
module DeterministicScore
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(checks, ordered_checks)
|
|
13
|
+
score = CheckWeights.call(checks)
|
|
14
|
+
score += BLOCKED_RISK_WEIGHT if ordered_checks.any? { |check| !check[:allowed] }
|
|
15
|
+
score.clamp(0.0, 1.0).round(4)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Risk
|
|
6
|
+
# Owns the deterministic risk model shared by scoring and the public API.
|
|
7
|
+
module Weights
|
|
8
|
+
FINDINGS = {
|
|
9
|
+
injection: [:injection_attempt, 0.50],
|
|
10
|
+
secret: [:leaked, 0.25],
|
|
11
|
+
pii: [:detected, 0.10],
|
|
12
|
+
policy: [:violated, 0.25]
|
|
13
|
+
}.freeze
|
|
14
|
+
BLOCKED = 0.15
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'risk/ai_score'
|
|
4
|
+
require_relative 'risk/deterministic_score'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
# Computes the bounded heuristic risk score from merged check results and
|
|
9
|
+
# an optional analyzer score.
|
|
10
|
+
class RiskScorer
|
|
11
|
+
def self.call(checks, ordered_checks, ai_result)
|
|
12
|
+
new(checks, ordered_checks, ai_result).call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(checks, ordered_checks, ai_result)
|
|
16
|
+
@checks = checks
|
|
17
|
+
@ordered_checks = ordered_checks
|
|
18
|
+
@ai_result = ai_result
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
deterministic = Risk::DeterministicScore.call(@checks, @ordered_checks)
|
|
23
|
+
ai_risk = Risk::AiScore.call(@ai_result)
|
|
24
|
+
ai_risk ? [deterministic, ai_risk].max.round(4) : deterministic
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'secrets/custom_pattern_source'
|
|
4
|
+
require_relative 'secrets/catalog_source'
|
|
5
|
+
require_relative 'secrets/confidentiality_source'
|
|
6
|
+
require_relative 'secrets/internal_endpoint_source'
|
|
7
|
+
require_relative 'secrets/finding_order'
|
|
8
|
+
require_relative 'secrets/source_set'
|
|
9
|
+
require_relative 'secrets/normalized_source_set'
|
|
10
|
+
|
|
11
|
+
module Olyx
|
|
12
|
+
module Guardrails
|
|
13
|
+
# Coordinates independent private secret-finding sources.
|
|
14
|
+
class SecretFindingCollector
|
|
15
|
+
def self.call(source, custom_patterns: [])
|
|
16
|
+
new(source.to_s, custom_patterns).call
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(source, custom_patterns)
|
|
20
|
+
@source = source
|
|
21
|
+
@custom_patterns = custom_patterns
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call
|
|
25
|
+
findings = Secrets::SourceSet.call(@source, @custom_patterns) +
|
|
26
|
+
Secrets::NormalizedSourceSet.call(@source, @custom_patterns)
|
|
27
|
+
Secrets::FindingOrder.call(findings)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'secret_finding_collector'
|
|
4
|
+
require_relative 'secrets/finding_presenter'
|
|
5
|
+
require_relative 'secrets/redactor'
|
|
6
|
+
require_relative 'secrets/blocked'
|
|
7
|
+
|
|
8
|
+
module Olyx
|
|
9
|
+
module Guardrails
|
|
10
|
+
# Public detect, redact, and exception-driven secret operations.
|
|
11
|
+
class SecretScanner
|
|
12
|
+
Blocked = Secrets::Blocked
|
|
13
|
+
|
|
14
|
+
def self.scan(text, custom_patterns: [])
|
|
15
|
+
findings = collect(text, custom_patterns)
|
|
16
|
+
{ leaked: findings.any?, findings: Secrets::FindingPresenter.call(findings) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.redact(text, custom_patterns: [])
|
|
20
|
+
source = text.to_s
|
|
21
|
+
findings = collect(source, custom_patterns)
|
|
22
|
+
{
|
|
23
|
+
text: findings.empty? ? source : Secrets::Redactor.call(source, findings),
|
|
24
|
+
leaked: findings.any?,
|
|
25
|
+
findings: Secrets::FindingPresenter.call(findings)
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.scan!(text, custom_patterns: [])
|
|
30
|
+
result = scan(text, custom_patterns: custom_patterns)
|
|
31
|
+
raise Blocked, result[:findings] if result[:leaked]
|
|
32
|
+
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.collect(text, custom_patterns)
|
|
37
|
+
SecretFindingCollector.call(text.to_s, custom_patterns: custom_patterns)
|
|
38
|
+
end
|
|
39
|
+
private_class_method :collect
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Secrets
|
|
6
|
+
# Raised by SecretScanner.scan! when secret findings make input unsafe.
|
|
7
|
+
class Blocked < StandardError
|
|
8
|
+
attr_reader :findings
|
|
9
|
+
|
|
10
|
+
def initialize(findings)
|
|
11
|
+
@findings = findings
|
|
12
|
+
super('Response blocked: secret leakage detected')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pattern_catalog'
|
|
4
|
+
require_relative 'private_network_validator'
|
|
5
|
+
require_relative 'regexp_finding_source'
|
|
6
|
+
|
|
7
|
+
module Olyx
|
|
8
|
+
module Guardrails
|
|
9
|
+
module Secrets
|
|
10
|
+
# Finds catalog secrets and validates private-network candidates.
|
|
11
|
+
module CatalogSource
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(source)
|
|
15
|
+
PatternCatalog::SIMPLE.flat_map { |category, pattern| findings(source, category, pattern) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def findings(source, category, pattern)
|
|
19
|
+
matches = RegexpFindingSource.call(source, category, pattern)
|
|
20
|
+
return matches unless category == 'private_network_address'
|
|
21
|
+
|
|
22
|
+
matches.select { |finding| PrivateNetworkValidator.call(finding[:full]) }
|
|
23
|
+
end
|
|
24
|
+
private_class_method :findings
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Secrets
|
|
6
|
+
# Cloud and SaaS provider credential patterns.
|
|
7
|
+
module CloudCredentialPatterns
|
|
8
|
+
AWS_ACCESS_KEY = /\b(AKIA|ASIA|AROA|ABIA|ACCA|AIPA)[A-Z0-9]{16}\b/
|
|
9
|
+
AWS_SECRET_KEY = %r{(?:aws[_\-\s]?(?:secret[_\-\s]?)?(?:access[_\-\s]?)?key|secret[_\-\s]access[_\-\s]key)[\s=:"']+([A-Za-z0-9/+=]{40})\b}i
|
|
10
|
+
STRIPE_KEY = /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}|\bwhsec_[A-Za-z0-9]{16,}/
|
|
11
|
+
GOOGLE_KEY = /\bAIza[0-9A-Za-z_-]{35}\b|\bGOCSPX-[0-9A-Za-z_-]{20,}\b/
|
|
12
|
+
AZURE_STORAGE_KEY = %r{(?:AccountKey|SharedAccessKey)=[A-Za-z0-9+/=]{40,}}i
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pattern_catalog'
|
|
4
|
+
require_relative 'regexp_finding_source'
|
|
5
|
+
|
|
6
|
+
module Olyx
|
|
7
|
+
module Guardrails
|
|
8
|
+
module Secrets
|
|
9
|
+
# Finds explicit confidentiality markers.
|
|
10
|
+
module ConfidentialitySource
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(source)
|
|
14
|
+
PatternCatalog::CONFIDENTIALITY.flat_map do |pattern|
|
|
15
|
+
RegexpFindingSource.call(source, 'confidentiality_marker', pattern)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Secrets
|
|
6
|
+
# Compiles bounded, non-empty custom secret patterns.
|
|
7
|
+
module CustomPatternCompiler
|
|
8
|
+
TIMEOUT = 0.1
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(pattern)
|
|
13
|
+
compiled = Regexp.new(pattern, Regexp::IGNORECASE, timeout: TIMEOUT)
|
|
14
|
+
raise ArgumentError, 'custom patterns must not match empty text' if compiled.match?('')
|
|
15
|
+
|
|
16
|
+
compiled
|
|
17
|
+
rescue RegexpError => error
|
|
18
|
+
raise ArgumentError, "invalid custom pattern #{pattern.inspect}: #{error.message}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../validation'
|
|
4
|
+
require_relative 'custom_pattern_compiler'
|
|
5
|
+
require_relative 'regexp_finding_source'
|
|
6
|
+
|
|
7
|
+
module Olyx
|
|
8
|
+
module Guardrails
|
|
9
|
+
module Secrets
|
|
10
|
+
# Compiles bounded custom patterns and returns their internal findings.
|
|
11
|
+
module CustomPatternSource
|
|
12
|
+
TIMEOUT_ERROR = defined?(Regexp::TimeoutError) ? Regexp::TimeoutError : RegexpError
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def call(source, patterns)
|
|
17
|
+
Validation.array_of!(patterns, String, name: 'custom_patterns')
|
|
18
|
+
patterns.flat_map { |pattern| findings(source, CustomPatternCompiler.call(pattern)) }
|
|
19
|
+
rescue TIMEOUT_ERROR
|
|
20
|
+
raise ArgumentError, 'custom pattern timed out'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def findings(source, pattern)
|
|
24
|
+
RegexpFindingSource.call(source, 'custom_pattern', pattern).reject { |finding| finding[:full].empty? }
|
|
25
|
+
end
|
|
26
|
+
private_class_method :findings
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Secrets
|
|
6
|
+
# Deduplicates and orders secret findings by private offsets and category.
|
|
7
|
+
module FindingOrder
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(findings)
|
|
11
|
+
key = ->(finding) { [finding[:start], finding[:end], finding[:category]] }
|
|
12
|
+
findings.uniq(&key).sort_by(&key)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Secrets
|
|
8
|
+
# Converts private findings into safe correlation records.
|
|
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
|
+
value = finding[:full]
|
|
18
|
+
{
|
|
19
|
+
category: finding[:category],
|
|
20
|
+
matched: mask(value),
|
|
21
|
+
fingerprint: "sha256:#{Digest::SHA256.hexdigest(value)[0, 12]}",
|
|
22
|
+
start: finding[:start],
|
|
23
|
+
end: finding[:end]
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def mask(value)
|
|
28
|
+
value.length < 12 ? '[REDACTED]' : "#{value[0, 4]}…#{value[-4, 4]}"
|
|
29
|
+
end
|
|
30
|
+
private_class_method :present, :mask
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Secrets
|
|
6
|
+
# Provider-independent credential and key material patterns.
|
|
7
|
+
module GenericCredentialPatterns
|
|
8
|
+
TOKEN = /\bghp_\w+|\bghs_\w+|\bgho_\w+|\bghr_\w+|\bxoxb-\S+|\bxoxp-\S+|\bxoxs-\S+|\bxoxe-\S+|\bglpat-\w+|\bgldt-\w+|\bnpm_\w+|\bSG\.[A-Za-z0-9._-]{20,}|\bey[A-Za-z0-9._-]{20,}\.[A-Za-z0-9._-]{20,}|\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{20,}|\bkey-\S+/i
|
|
9
|
+
JWT = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/
|
|
10
|
+
PRIVATE_KEY = /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----.*?-----END (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/m
|
|
11
|
+
DATABASE_URL = %r{\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp)://[^\s:@/]+:[^\s@/]+@[^\s]+}i
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'network_patterns'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Secrets
|
|
8
|
+
# Expands internal-host suffix matches to the complete endpoint token.
|
|
9
|
+
module InternalEndpointSource
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(source)
|
|
13
|
+
source.to_enum(:scan, NetworkPatterns::INTERNAL_SUFFIX).map do
|
|
14
|
+
finding(source, Regexp.last_match)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def finding(source, match)
|
|
19
|
+
ending = match.end(0)
|
|
20
|
+
boundary = source[0...match.begin(0)].rindex(/[\s"']/)
|
|
21
|
+
start = boundary ? boundary + 1 : 0
|
|
22
|
+
{ category: 'internal_endpoint', full: source[start...ending], start: start, end: ending }
|
|
23
|
+
end
|
|
24
|
+
private_class_method :finding
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Secrets
|
|
6
|
+
# Patterns for private network coordinates that should not leave a trust boundary.
|
|
7
|
+
module NetworkPatterns
|
|
8
|
+
INTERNAL_SUFFIX = %r{\.internal\b|\.corp\b|\.intranet\b|\.local/|\bvpc\.|\.private\.|\.lan[/:]}i
|
|
9
|
+
PRIVATE_IP = %r{(?:https?://|://|host[=:\s]+|endpoint[=:\s]+|@)(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})}i
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../text/mapped_normalization'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Secrets
|
|
8
|
+
# Runs secret sources on normalized text and restores original offsets.
|
|
9
|
+
module NormalizedSourceSet
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(source, custom_patterns)
|
|
13
|
+
mapped = Text::MappedNormalization.new(source)
|
|
14
|
+
return [] unless mapped.changed?
|
|
15
|
+
|
|
16
|
+
SourceSet.call(mapped.text, custom_patterns).map { |finding| restore(source, mapped, finding) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def restore(source, mapped, finding)
|
|
20
|
+
starting, ending = mapped.original_span(finding[:start], finding[:end])
|
|
21
|
+
finding.merge(full: source[starting...ending], start: starting, end: ending)
|
|
22
|
+
end
|
|
23
|
+
private_class_method :restore
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'cloud_credential_patterns'
|
|
4
|
+
require_relative 'generic_credential_patterns'
|
|
5
|
+
require_relative 'network_patterns'
|
|
6
|
+
|
|
7
|
+
module Olyx
|
|
8
|
+
module Guardrails
|
|
9
|
+
module Secrets
|
|
10
|
+
# Declarative catalog for built-in confidentiality and credential forms.
|
|
11
|
+
module PatternCatalog
|
|
12
|
+
MARKERS = [
|
|
13
|
+
'confidential', 'proprietary', 'restricted', 'internal use only',
|
|
14
|
+
'not for distribution', 'do not share', 'do not distribute', 'top secret',
|
|
15
|
+
'trade secret', 'need to know', 'company confidential',
|
|
16
|
+
'attorney-client privilege', 'attorney client privilege', 'work product',
|
|
17
|
+
'privileged and confidential'
|
|
18
|
+
].freeze
|
|
19
|
+
CONFIDENTIALITY = MARKERS.map do |marker|
|
|
20
|
+
Regexp.new(Regexp.escape(marker), Regexp::IGNORECASE)
|
|
21
|
+
end.freeze
|
|
22
|
+
SIMPLE = {
|
|
23
|
+
'private_network_address' => NetworkPatterns::PRIVATE_IP,
|
|
24
|
+
'aws_access_key' => CloudCredentialPatterns::AWS_ACCESS_KEY,
|
|
25
|
+
'aws_secret_key' => CloudCredentialPatterns::AWS_SECRET_KEY,
|
|
26
|
+
'secret_token' => GenericCredentialPatterns::TOKEN,
|
|
27
|
+
'jwt' => GenericCredentialPatterns::JWT,
|
|
28
|
+
'private_key' => GenericCredentialPatterns::PRIVATE_KEY,
|
|
29
|
+
'database_url' => GenericCredentialPatterns::DATABASE_URL,
|
|
30
|
+
'stripe_key' => CloudCredentialPatterns::STRIPE_KEY,
|
|
31
|
+
'google_key' => CloudCredentialPatterns::GOOGLE_KEY,
|
|
32
|
+
'azure_storage_key' => CloudCredentialPatterns::AZURE_STORAGE_KEY
|
|
33
|
+
}.freeze
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Secrets
|
|
6
|
+
# Rejects private-network candidates with impossible IPv4 octets.
|
|
7
|
+
module PrivateNetworkValidator
|
|
8
|
+
ADDRESS = /\b(?:10|172|192)\.(?:\d{1,3}\.){2}\d{1,3}\b/
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(value)
|
|
13
|
+
address = value[ADDRESS]
|
|
14
|
+
address&.split('.')&.all? { |octet| octet.to_i.between?(0, 255) }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Secrets
|
|
6
|
+
# Normalizes and merges overlapping secret redaction offsets.
|
|
7
|
+
module RedactionSpans
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(findings)
|
|
11
|
+
spans = findings.filter_map { |finding| span(finding) }.sort
|
|
12
|
+
spans.each_with_object([]) { |candidate, merged| append(merged, candidate) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def span(finding)
|
|
16
|
+
start = finding[:start]
|
|
17
|
+
ending = finding[:end]
|
|
18
|
+
[start, ending] if ending > start
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def append(merged, candidate)
|
|
22
|
+
previous = merged.last
|
|
23
|
+
previous_end = previous&.last
|
|
24
|
+
return merged << candidate unless previous_end && candidate.first <= previous_end
|
|
25
|
+
|
|
26
|
+
previous[1] = [previous_end, candidate.last].max
|
|
27
|
+
end
|
|
28
|
+
private_class_method :span, :append
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'redaction_spans'
|
|
4
|
+
|
|
5
|
+
module Olyx
|
|
6
|
+
module Guardrails
|
|
7
|
+
module Secrets
|
|
8
|
+
# Applies confidentiality-safe redaction to private findings.
|
|
9
|
+
module Redactor
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(text, findings)
|
|
13
|
+
return '[REDACTED]' if confidential?(findings)
|
|
14
|
+
|
|
15
|
+
RedactionSpans.call(findings).reverse_each.with_object(text.dup) do |(start, ending), output|
|
|
16
|
+
output[start...ending] = '[REDACTED]'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def confidential?(findings)
|
|
21
|
+
findings.any? { |finding| finding[:category] == 'confidentiality_marker' }
|
|
22
|
+
end
|
|
23
|
+
private_class_method :confidential?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Secrets
|
|
6
|
+
# Converts every Regexp match into an internal offset-aware finding.
|
|
7
|
+
module RegexpFindingSource
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(source, category, pattern)
|
|
11
|
+
source.to_enum(:scan, pattern).map { finding(category, Regexp.last_match) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def finding(category, match)
|
|
15
|
+
{ category: category, full: match[0].to_s, start: match.begin(0), end: match.end(0) }
|
|
16
|
+
end
|
|
17
|
+
private_class_method :finding
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Olyx
|
|
4
|
+
module Guardrails
|
|
5
|
+
module Secrets
|
|
6
|
+
# Combines independent built-in and caller-defined secret sources.
|
|
7
|
+
module SourceSet
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def call(source, custom_patterns)
|
|
11
|
+
ConfidentialitySource.call(source) +
|
|
12
|
+
InternalEndpointSource.call(source) +
|
|
13
|
+
CatalogSource.call(source) +
|
|
14
|
+
CustomPatternSource.call(source, custom_patterns)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'injection_patterns'
|
|
4
|
+
require_relative 'message_content'
|
|
5
|
+
require_relative 'text/detection_variants'
|
|
6
|
+
|
|
7
|
+
module Olyx
|
|
8
|
+
module Guardrails
|
|
9
|
+
# Detects injection patterns within one chat message.
|
|
10
|
+
module SingleMessageInjectionDetector
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(message)
|
|
14
|
+
content = MessageContent.text(message)
|
|
15
|
+
return [] if content.strip.empty?
|
|
16
|
+
|
|
17
|
+
role = MessageContent.role(message)
|
|
18
|
+
scan(content, role)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def scan(content, role)
|
|
22
|
+
findings = Text::DetectionVariants.call(content).flat_map do |variant|
|
|
23
|
+
InjectionPatterns::SINGLE_MESSAGE.filter_map { |pattern| finding(variant, role, pattern) }
|
|
24
|
+
end
|
|
25
|
+
findings.uniq { |finding| finding[:match] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def finding(content, role, pattern)
|
|
29
|
+
match = content.match(pattern)
|
|
30
|
+
{ role: role.empty? ? 'unknown' : role, match: match[0].strip } if match
|
|
31
|
+
end
|
|
32
|
+
private_class_method :finding, :scan
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|