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.
Files changed (234) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +117 -0
  3. data/CODE_OF_CONDUCT.md +28 -0
  4. data/CONTRIBUTING.md +63 -0
  5. data/LICENSE +134 -0
  6. data/README.md +611 -0
  7. data/SECURITY.md +35 -0
  8. data/docs/API.md +460 -0
  9. data/examples/claude_analyzer.rb +53 -0
  10. data/examples/custom_policy.rb +41 -0
  11. data/examples/notifier.rb +37 -0
  12. data/examples/openai_analyzer.rb +45 -0
  13. data/examples/rails_opt_in.rb +99 -0
  14. data/examples/ruby_only.rb +53 -0
  15. data/lib/generators/olyx_guardrails/install_generator.rb +22 -0
  16. data/lib/generators/olyx_guardrails/templates/initializer.rb.tt +16 -0
  17. data/lib/generators/olyx_guardrails/templates/policy.yml.tt +41 -0
  18. data/lib/olyx/guardrails/ai/analysis_normalizer.rb +26 -0
  19. data/lib/olyx/guardrails/ai/analysis_pipeline.rb +26 -0
  20. data/lib/olyx/guardrails/ai/boolean_validator.rb +18 -0
  21. data/lib/olyx/guardrails/ai/flag_finding_merger.rb +16 -0
  22. data/lib/olyx/guardrails/ai/result_sanitizer.rb +29 -0
  23. data/lib/olyx/guardrails/ai/secret_finding_merger.rb +19 -0
  24. data/lib/olyx/guardrails/ai/standard_finding_merger.rb +24 -0
  25. data/lib/olyx/guardrails/ai_analysis.rb +29 -0
  26. data/lib/olyx/guardrails/ai_context_builder.rb +32 -0
  27. data/lib/olyx/guardrails/ai_failure_handler.rb +17 -0
  28. data/lib/olyx/guardrails/ai_finding_merger.rb +30 -0
  29. data/lib/olyx/guardrails/check_analyzer.rb +26 -0
  30. data/lib/olyx/guardrails/check_base_result.rb +32 -0
  31. data/lib/olyx/guardrails/check_pipeline.rb +17 -0
  32. data/lib/olyx/guardrails/check_result_builder.rb +24 -0
  33. data/lib/olyx/guardrails/check_runner.rb +42 -0
  34. data/lib/olyx/guardrails/check_set.rb +33 -0
  35. data/lib/olyx/guardrails/checks/injection_check.rb +20 -0
  36. data/lib/olyx/guardrails/checks/injection_result.rb +22 -0
  37. data/lib/olyx/guardrails/checks/length_check.rb +18 -0
  38. data/lib/olyx/guardrails/checks/message_injection_check.rb +19 -0
  39. data/lib/olyx/guardrails/checks/pii_check.rb +19 -0
  40. data/lib/olyx/guardrails/checks/policy_check.rb +26 -0
  41. data/lib/olyx/guardrails/checks/secret_check.rb +30 -0
  42. data/lib/olyx/guardrails/checks/skipped_checks.rb +26 -0
  43. data/lib/olyx/guardrails/enum_value.rb +20 -0
  44. data/lib/olyx/guardrails/errors.rb +22 -0
  45. data/lib/olyx/guardrails/injection_detector.rb +27 -0
  46. data/lib/olyx/guardrails/injection_patterns.rb +44 -0
  47. data/lib/olyx/guardrails/integrations/openai/analyzer_setup.rb +21 -0
  48. data/lib/olyx/guardrails/integrations/openai/configuration_resolver.rb +28 -0
  49. data/lib/olyx/guardrails/integrations/openai/dependency_contracts.rb +28 -0
  50. data/lib/olyx/guardrails/integrations/openai/input_builder.rb +39 -0
  51. data/lib/olyx/guardrails/integrations/openai/member_reader.rb +21 -0
  52. data/lib/olyx/guardrails/integrations/openai/model_identifier.rb +28 -0
  53. data/lib/olyx/guardrails/integrations/openai/refusal_guard.rb +28 -0
  54. data/lib/olyx/guardrails/integrations/openai/request_builder.rb +36 -0
  55. data/lib/olyx/guardrails/integrations/openai/request_configuration.rb +25 -0
  56. data/lib/olyx/guardrails/integrations/openai/request_values.rb +33 -0
  57. data/lib/olyx/guardrails/integrations/openai/response_contents.rb +22 -0
  58. data/lib/olyx/guardrails/integrations/openai/response_option_keys.rb +32 -0
  59. data/lib/olyx/guardrails/integrations/openai/response_options.rb +23 -0
  60. data/lib/olyx/guardrails/integrations/openai/schema_registry.rb +46 -0
  61. data/lib/olyx/guardrails/integrations/openai/sdk.rb +26 -0
  62. data/lib/olyx/guardrails/integrations/openai/signal_summary.rb +40 -0
  63. data/lib/olyx/guardrails/integrations/openai_analyzer.rb +107 -0
  64. data/lib/olyx/guardrails/integrations/openai_analyzer_configuration.rb +38 -0
  65. data/lib/olyx/guardrails/integrations/openai_response_parser.rb +34 -0
  66. data/lib/olyx/guardrails/message_check_runner.rb +24 -0
  67. data/lib/olyx/guardrails/message_check_set.rb +20 -0
  68. data/lib/olyx/guardrails/message_content.rb +27 -0
  69. data/lib/olyx/guardrails/message_source.rb +16 -0
  70. data/lib/olyx/guardrails/multi_turn_injection_detector.rb +18 -0
  71. data/lib/olyx/guardrails/multi_turn_pair_scanner.rb +35 -0
  72. data/lib/olyx/guardrails/multi_turn_variant_pairs.rb +25 -0
  73. data/lib/olyx/guardrails/notification/base_event.rb +35 -0
  74. data/lib/olyx/guardrails/notification/deep_freezer.rb +25 -0
  75. data/lib/olyx/guardrails/notification/delivery_dispatcher.rb +38 -0
  76. data/lib/olyx/guardrails/notification/delivery_summary.rb +17 -0
  77. data/lib/olyx/guardrails/notification/handler.rb +33 -0
  78. data/lib/olyx/guardrails/notification/handler_collection.rb +25 -0
  79. data/lib/olyx/guardrails/notification/handler_collection_validator.rb +23 -0
  80. data/lib/olyx/guardrails/notification/key.rb +19 -0
  81. data/lib/olyx/guardrails/notification/metadata.rb +34 -0
  82. data/lib/olyx/guardrails/notification/notifier_setup.rb +19 -0
  83. data/lib/olyx/guardrails/notification/preview.rb +27 -0
  84. data/lib/olyx/guardrails/notification/scrubber.rb +22 -0
  85. data/lib/olyx/guardrails/notification/violation_labels.rb +19 -0
  86. data/lib/olyx/guardrails/notification_event_builder.rb +41 -0
  87. data/lib/olyx/guardrails/notification_sanitizer.rb +33 -0
  88. data/lib/olyx/guardrails/notifier.rb +53 -0
  89. data/lib/olyx/guardrails/notifier_configuration.rb +30 -0
  90. data/lib/olyx/guardrails/pii/ambiguous_numeric_date.rb +32 -0
  91. data/lib/olyx/guardrails/pii/block_scrubber.rb +31 -0
  92. data/lib/olyx/guardrails/pii/content_scrubber.rb +33 -0
  93. data/lib/olyx/guardrails/pii/date_validator.rb +25 -0
  94. data/lib/olyx/guardrails/pii/hash_key.rb +19 -0
  95. data/lib/olyx/guardrails/pii/iban_remainder.rb +25 -0
  96. data/lib/olyx/guardrails/pii/iban_validator.rb +34 -0
  97. data/lib/olyx/guardrails/pii/ipv4_validator.rb +17 -0
  98. data/lib/olyx/guardrails/pii/ipv6_validator.rb +20 -0
  99. data/lib/olyx/guardrails/pii/luhn_checksum.rb +29 -0
  100. data/lib/olyx/guardrails/pii/luhn_validator.rb +21 -0
  101. data/lib/olyx/guardrails/pii/message_scrubber.rb +31 -0
  102. data/lib/olyx/guardrails/pii/named_date_validator.rb +28 -0
  103. data/lib/olyx/guardrails/pii/numeric_date_validator.rb +26 -0
  104. data/lib/olyx/guardrails/pii/pattern_catalog.rb +41 -0
  105. data/lib/olyx/guardrails/pii/sin_validator.rb +24 -0
  106. data/lib/olyx/guardrails/pii/ssn_validator.rb +21 -0
  107. data/lib/olyx/guardrails/pii/text_scrubber.rb +28 -0
  108. data/lib/olyx/guardrails/pii/year_first_date.rb +18 -0
  109. data/lib/olyx/guardrails/pii_scrubber.rb +23 -0
  110. data/lib/olyx/guardrails/pii_validators.rb +26 -0
  111. data/lib/olyx/guardrails/policy/ai_failure_mode.rb +15 -0
  112. data/lib/olyx/guardrails/policy/configuration.rb +62 -0
  113. data/lib/olyx/guardrails/policy/configuration_hash.rb +20 -0
  114. data/lib/olyx/guardrails/policy/finding_order.rb +20 -0
  115. data/lib/olyx/guardrails/policy/finding_presenter.rb +41 -0
  116. data/lib/olyx/guardrails/policy/match_collector.rb +20 -0
  117. data/lib/olyx/guardrails/policy/name.rb +23 -0
  118. data/lib/olyx/guardrails/policy/normalized_pattern_matcher.rb +34 -0
  119. data/lib/olyx/guardrails/policy/pattern_matcher.rb +19 -0
  120. data/lib/olyx/guardrails/policy/redaction_result.rb +20 -0
  121. data/lib/olyx/guardrails/policy/redaction_spans.rb +32 -0
  122. data/lib/olyx/guardrails/policy/rule_collection.rb +34 -0
  123. data/lib/olyx/guardrails/policy/rule_matcher.rb +29 -0
  124. data/lib/olyx/guardrails/policy/rule_normalizer.rb +21 -0
  125. data/lib/olyx/guardrails/policy/scan_result.rb +21 -0
  126. data/lib/olyx/guardrails/policy/secret_pattern_collection.rb +22 -0
  127. data/lib/olyx/guardrails/policy/unmatched_segment_builder.rb +28 -0
  128. data/lib/olyx/guardrails/policy/unmatched_transformer.rb +20 -0
  129. data/lib/olyx/guardrails/policy.rb +46 -0
  130. data/lib/olyx/guardrails/policy_aware_redactor.rb +42 -0
  131. data/lib/olyx/guardrails/policy_redactor.rb +28 -0
  132. data/lib/olyx/guardrails/policy_rule/configuration.rb +46 -0
  133. data/lib/olyx/guardrails/policy_rule/description_value.rb +24 -0
  134. data/lib/olyx/guardrails/policy_rule/match_mode.rb +15 -0
  135. data/lib/olyx/guardrails/policy_rule/name_value.rb +22 -0
  136. data/lib/olyx/guardrails/policy_rule/pattern_compiler.rb +28 -0
  137. data/lib/olyx/guardrails/policy_rule/regexp_compiler.rb +31 -0
  138. data/lib/olyx/guardrails/policy_rule/replacement_value.rb +26 -0
  139. data/lib/olyx/guardrails/policy_rule/term_compiler.rb +33 -0
  140. data/lib/olyx/guardrails/policy_rule/text_values.rb +23 -0
  141. data/lib/olyx/guardrails/policy_rule/values.rb +33 -0
  142. data/lib/olyx/guardrails/policy_rule.rb +30 -0
  143. data/lib/olyx/guardrails/policy_scanner.rb +44 -0
  144. data/lib/olyx/guardrails/rails/action_cable.rb +21 -0
  145. data/lib/olyx/guardrails/rails/active_job_handler.rb +27 -0
  146. data/lib/olyx/guardrails/rails/active_model_validator.rb +23 -0
  147. data/lib/olyx/guardrails/rails/check_telemetry.rb +19 -0
  148. data/lib/olyx/guardrails/rails/configuration.rb +68 -0
  149. data/lib/olyx/guardrails/rails/configuration_finalizer.rb +30 -0
  150. data/lib/olyx/guardrails/rails/configuration_registry.rb +41 -0
  151. data/lib/olyx/guardrails/rails/configuration_values.rb +30 -0
  152. data/lib/olyx/guardrails/rails/constant_resolver.rb +18 -0
  153. data/lib/olyx/guardrails/rails/controller.rb +34 -0
  154. data/lib/olyx/guardrails/rails/controller_metadata.rb +22 -0
  155. data/lib/olyx/guardrails/rails/decision_service.rb +26 -0
  156. data/lib/olyx/guardrails/rails/enforcer.rb +33 -0
  157. data/lib/olyx/guardrails/rails/evaluation_lifecycle.rb +23 -0
  158. data/lib/olyx/guardrails/rails/evaluation_service.rb +12 -0
  159. data/lib/olyx/guardrails/rails/filter_parameters.rb +26 -0
  160. data/lib/olyx/guardrails/rails/graphql.rb +21 -0
  161. data/lib/olyx/guardrails/rails/ingress.rb +26 -0
  162. data/lib/olyx/guardrails/rails/input_runtime.rb +25 -0
  163. data/lib/olyx/guardrails/rails/instrumentation.rb +39 -0
  164. data/lib/olyx/guardrails/rails/instrumentation_payloads.rb +25 -0
  165. data/lib/olyx/guardrails/rails/instrumentation_publisher.rb +24 -0
  166. data/lib/olyx/guardrails/rails/integration_configuration.rb +27 -0
  167. data/lib/olyx/guardrails/rails/job.rb +37 -0
  168. data/lib/olyx/guardrails/rails/job_argument.rb +41 -0
  169. data/lib/olyx/guardrails/rails/job_reference.rb +36 -0
  170. data/lib/olyx/guardrails/rails/message_evaluation_service.rb +13 -0
  171. data/lib/olyx/guardrails/rails/message_runtime.rb +20 -0
  172. data/lib/olyx/guardrails/rails/notification_dispatcher.rb +22 -0
  173. data/lib/olyx/guardrails/rails/output_runtime.rb +26 -0
  174. data/lib/olyx/guardrails/rails/path_value.rb +30 -0
  175. data/lib/olyx/guardrails/rails/policy_configuration.rb +25 -0
  176. data/lib/olyx/guardrails/rails/policy_document.rb +40 -0
  177. data/lib/olyx/guardrails/rails/policy_file.rb +29 -0
  178. data/lib/olyx/guardrails/rails/policy_file_error.rb +25 -0
  179. data/lib/olyx/guardrails/rails/policy_rule_names.rb +19 -0
  180. data/lib/olyx/guardrails/rails/policy_yaml.rb +22 -0
  181. data/lib/olyx/guardrails/rails/queue_name.rb +21 -0
  182. data/lib/olyx/guardrails/rails/redaction_payload.rb +36 -0
  183. data/lib/olyx/guardrails/rails/redaction_service.rb +21 -0
  184. data/lib/olyx/guardrails/rails/result_summary.rb +36 -0
  185. data/lib/olyx/guardrails/rails/runtime.rb +30 -0
  186. data/lib/olyx/guardrails/rails/timer.rb +27 -0
  187. data/lib/olyx/guardrails/rails/upload.rb +34 -0
  188. data/lib/olyx/guardrails/rails.rb +35 -0
  189. data/lib/olyx/guardrails/railtie.rb +19 -0
  190. data/lib/olyx/guardrails/redaction/content_result.rb +32 -0
  191. data/lib/olyx/guardrails/redaction/input_validator.rb +17 -0
  192. data/lib/olyx/guardrails/redaction/public_result.rb +23 -0
  193. data/lib/olyx/guardrails/redaction/unrestricted_text.rb +17 -0
  194. data/lib/olyx/guardrails/redactor.rb +29 -0
  195. data/lib/olyx/guardrails/risk/ai_score.rb +19 -0
  196. data/lib/olyx/guardrails/risk/check_weights.rb +18 -0
  197. data/lib/olyx/guardrails/risk/deterministic_score.rb +20 -0
  198. data/lib/olyx/guardrails/risk/weights.rb +18 -0
  199. data/lib/olyx/guardrails/risk_scorer.rb +28 -0
  200. data/lib/olyx/guardrails/secret_finding_collector.rb +31 -0
  201. data/lib/olyx/guardrails/secret_scanner.rb +42 -0
  202. data/lib/olyx/guardrails/secrets/blocked.rb +17 -0
  203. data/lib/olyx/guardrails/secrets/catalog_source.rb +28 -0
  204. data/lib/olyx/guardrails/secrets/cloud_credential_patterns.rb +16 -0
  205. data/lib/olyx/guardrails/secrets/confidentiality_source.rb +21 -0
  206. data/lib/olyx/guardrails/secrets/custom_pattern_compiler.rb +23 -0
  207. data/lib/olyx/guardrails/secrets/custom_pattern_source.rb +30 -0
  208. data/lib/olyx/guardrails/secrets/finding_order.rb +17 -0
  209. data/lib/olyx/guardrails/secrets/finding_presenter.rb +34 -0
  210. data/lib/olyx/guardrails/secrets/generic_credential_patterns.rb +15 -0
  211. data/lib/olyx/guardrails/secrets/internal_endpoint_source.rb +28 -0
  212. data/lib/olyx/guardrails/secrets/network_patterns.rb +13 -0
  213. data/lib/olyx/guardrails/secrets/normalized_source_set.rb +27 -0
  214. data/lib/olyx/guardrails/secrets/pattern_catalog.rb +37 -0
  215. data/lib/olyx/guardrails/secrets/private_network_validator.rb +19 -0
  216. data/lib/olyx/guardrails/secrets/redaction_spans.rb +32 -0
  217. data/lib/olyx/guardrails/secrets/redactor.rb +27 -0
  218. data/lib/olyx/guardrails/secrets/regexp_finding_source.rb +21 -0
  219. data/lib/olyx/guardrails/secrets/source_set.rb +19 -0
  220. data/lib/olyx/guardrails/single_message_injection_detector.rb +35 -0
  221. data/lib/olyx/guardrails/supplemental_violation_labels.rb +22 -0
  222. data/lib/olyx/guardrails/text/base64_decoder.rb +45 -0
  223. data/lib/olyx/guardrails/text/detection_variants.rb +28 -0
  224. data/lib/olyx/guardrails/text/html_decoder.rb +18 -0
  225. data/lib/olyx/guardrails/text/mapped_builder.rb +33 -0
  226. data/lib/olyx/guardrails/text/mapped_normalization.rb +27 -0
  227. data/lib/olyx/guardrails/text/normalizer.rb +35 -0
  228. data/lib/olyx/guardrails/text/unicode_escape_decoder.rb +20 -0
  229. data/lib/olyx/guardrails/text/url_decoder.rb +20 -0
  230. data/lib/olyx/guardrails/validation.rb +33 -0
  231. data/lib/olyx/guardrails/version.rb +8 -0
  232. data/lib/olyx/guardrails/violation_labels.rb +24 -0
  233. data/lib/olyx/guardrails.rb +63 -0
  234. 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