olyx-guardrails 1.1.1 → 1.2.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 +4 -4
- data/CHANGELOG.md +37 -7
- data/README.md +3 -2
- data/lib/olyx/guardrails/check_result_builder.rb +1 -1
- data/lib/olyx/guardrails/check_runner.rb +4 -2
- data/lib/olyx/guardrails/check_set.rb +66 -19
- data/lib/olyx/guardrails/injection_patterns.rb +1 -2
- data/lib/olyx/guardrails/llm_analysis.rb +4 -2
- data/lib/olyx/guardrails/message_check_runner.rb +6 -4
- data/lib/olyx/guardrails/pii/text_scrubber.rb +15 -1
- data/lib/olyx/guardrails/policy/match_collector.rb +7 -1
- data/lib/olyx/guardrails/policy/rule_matcher.rb +26 -9
- data/lib/olyx/guardrails/policy.rb +37 -24
- data/lib/olyx/guardrails/policy_rule.rb +56 -19
- data/lib/olyx/guardrails/rails.rb +8 -7
- data/lib/olyx/guardrails/redaction/content_result.rb +4 -3
- data/lib/olyx/guardrails/risk/deterministic_score.rb +2 -4
- data/lib/olyx/guardrails/risk/weights.rb +0 -1
- data/lib/olyx/guardrails/risk_scorer.rb +4 -5
- data/lib/olyx/guardrails/secret_finding_collector.rb +0 -1
- data/lib/olyx/guardrails/secret_scanner.rb +1 -2
- data/lib/olyx/guardrails/secrets/pattern_catalog.rb +0 -10
- data/lib/olyx/guardrails/secrets/redactor.rb +0 -7
- data/lib/olyx/guardrails/secrets/source_set.rb +1 -2
- data/lib/olyx/guardrails/text/detection_variants.rb +13 -1
- data/lib/olyx/guardrails/version.rb +1 -1
- data/lib/olyx/guardrails.rb +0 -1
- metadata +1 -21
- data/lib/olyx/guardrails/check_pipeline.rb +0 -17
- data/lib/olyx/guardrails/checks/injection_check.rb +0 -20
- data/lib/olyx/guardrails/checks/injection_result.rb +0 -22
- data/lib/olyx/guardrails/checks/length_check.rb +0 -18
- data/lib/olyx/guardrails/checks/message_injection_check.rb +0 -19
- data/lib/olyx/guardrails/checks/pii_check.rb +0 -19
- data/lib/olyx/guardrails/checks/policy_check.rb +0 -26
- data/lib/olyx/guardrails/checks/secret_check.rb +0 -30
- data/lib/olyx/guardrails/checks/skipped_checks.rb +0 -26
- data/lib/olyx/guardrails/message_check_set.rb +0 -20
- data/lib/olyx/guardrails/policy/configuration.rb +0 -68
- data/lib/olyx/guardrails/policy/normalized_pattern_matcher.rb +0 -34
- data/lib/olyx/guardrails/policy/pattern_matcher.rb +0 -19
- data/lib/olyx/guardrails/policy_rule/configuration.rb +0 -52
- data/lib/olyx/guardrails/policy_rule/description_value.rb +0 -24
- data/lib/olyx/guardrails/policy_rule/name_value.rb +0 -22
- data/lib/olyx/guardrails/policy_rule/replacement_value.rb +0 -26
- data/lib/olyx/guardrails/policy_rule/text_values.rb +0 -23
- data/lib/olyx/guardrails/policy_rule/values.rb +0 -33
- data/lib/olyx/guardrails/secrets/confidentiality_source.rb +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1ee4f7c6297bf8348c6d74feff8bd055ce23b819091893043d02a6695140cd2
|
|
4
|
+
data.tar.gz: e06e7f54f777a8e56da1fcbad45f92d327f30aea934467235a9bc4384dc2229d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d85a8e9c03dc058fd188132a2754c4738063ddc79039eb5bd88c5dcf25555f3b266d6d4b29ee37503b7c25fe0beabc74d0cfe895f82ad16c82e93ff2351955b
|
|
7
|
+
data.tar.gz: 6e04d4fda8672b775475e0741b621ee850ab178656283a20c4fc1f27aa324112cb2ee5c883890dec97b53d423b7f65e6daa88006269574481c36e69128542afd
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,39 @@
|
|
|
3
3
|
All notable changes to olyx-guardrails are documented here.
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [1.2.0] - 2026-10-02
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Prompt-injection normalization now scans long inputs in overlapping bounded
|
|
13
|
+
windows instead of ignoring content after the first 20,000 characters.
|
|
14
|
+
- Narrowed role-play matching so ordinary agent prompts do not block by
|
|
15
|
+
default.
|
|
16
|
+
- Provider exceptions return a stable public error instead of exposing the
|
|
17
|
+
provider's exception message.
|
|
18
|
+
- Confidentiality labels are no longer classified as credentials or used to
|
|
19
|
+
redact an entire input. Applications can enforce those labels with policy
|
|
20
|
+
rules when required.
|
|
21
|
+
- Risk scores now describe findings independently from the policy's blocking
|
|
22
|
+
configuration.
|
|
23
|
+
- Removed redundant scanner passes and consolidated internal configuration and
|
|
24
|
+
matching layers.
|
|
25
|
+
- Simplified the public quality gate to tests, documentation, and RuboCop.
|
|
26
|
+
|
|
27
|
+
## [1.1.3] - 2026-08-07
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Added the Bundler release task expected by the trusted publishing workflow.
|
|
32
|
+
|
|
33
|
+
## [1.1.2] - 2026-08-07
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Added a tag-based RubyGems release workflow using trusted publishing.
|
|
38
|
+
|
|
6
39
|
## [1.1.1] - 2026-07-24
|
|
7
40
|
|
|
8
41
|
### Changed
|
|
@@ -27,7 +60,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
27
60
|
decision has zero risk.
|
|
28
61
|
- The Rails generator provides copyable policy customization instructions and
|
|
29
62
|
tests the customized YAML path.
|
|
30
|
-
- Contributor setup,
|
|
63
|
+
- Contributor setup, local validation, and maintainer release
|
|
31
64
|
verification have canonical commands and documentation.
|
|
32
65
|
- Closely coupled proxy objects were folded into their owning runtime,
|
|
33
66
|
configuration, result-building, and notification components.
|
|
@@ -35,7 +68,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
35
68
|
ignored instead of being coerced across the untrusted provider boundary.
|
|
36
69
|
- Contributor and Rails appraisal locks use the current `net-imap` patch.
|
|
37
70
|
- CI refreshes the Ruby advisory database and blocks known-vulnerable locked
|
|
38
|
-
dependencies without making
|
|
71
|
+
dependencies without making offline local validation network-dependent.
|
|
39
72
|
|
|
40
73
|
## [1.0.0] - 2026-07-21
|
|
41
74
|
|
|
@@ -143,11 +176,8 @@ Initial public release.
|
|
|
143
176
|
|
|
144
177
|
- The core has one lightweight runtime dependency, Ruby's `base64` bundled gem.
|
|
145
178
|
Rails remains optional and is loaded only when used.
|
|
146
|
-
- CI enforces RuboCop
|
|
147
|
-
|
|
148
|
-
`ClassLength`) in place of a separate complexity tool — and a RubyCritic
|
|
149
|
-
maintainability gate, alongside the Appraisal matrix across Rails 8.0
|
|
150
|
-
and 8.1.
|
|
179
|
+
- CI enforces tests, documentation checks, RuboCop, and the Appraisal matrix
|
|
180
|
+
across Rails 8.0 and 8.1.
|
|
151
181
|
- Native RDoc covers every supported public class, module, constant, attribute,
|
|
152
182
|
and method. CI blocks undocumented additions to the explicit public API
|
|
153
183
|
manifest while leaving implementation-only constants outside the
|
data/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Olyx Guardrails
|
|
2
2
|
|
|
3
|
-
[](https://rubygems.org/gems/olyx-guardrails)
|
|
4
|
+
[](https://github.com/Olyx-labs/olyx-guardrails/releases/latest)
|
|
4
5
|
[](https://github.com/Olyx-labs/olyx-guardrails/actions/workflows/test.yml)
|
|
5
6
|
[](https://securityscorecards.dev/viewer/?uri=github.com/Olyx-labs/olyx-guardrails)
|
|
6
7
|
[](LICENSE)
|
|
@@ -28,7 +29,7 @@ discover model calls, or send application content to a third party.
|
|
|
28
29
|
|
|
29
30
|
Rails is optional. The standalone Ruby API does not load Rails.
|
|
30
31
|
|
|
31
|
-
The 1.1 release supports Rails 8.0 and 8.1. Only the listed Rails
|
|
32
|
+
The 1.1 release line supports Rails 8.0 and 8.1. Only the listed Rails
|
|
32
33
|
series are tested and supported. When a series reaches upstream end-of-life,
|
|
33
34
|
a subsequent gem release may remove it instead of maintaining framework
|
|
34
35
|
security fixes independently. Every support change is recorded in the
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require_relative 'check_result_builder'
|
|
4
4
|
require_relative 'check_analyzer'
|
|
5
5
|
require_relative 'check_set'
|
|
6
|
-
require_relative 'check_pipeline'
|
|
7
6
|
require_relative 'policy'
|
|
8
7
|
require_relative 'validation'
|
|
9
8
|
|
|
@@ -24,7 +23,10 @@ module Olyx
|
|
|
24
23
|
|
|
25
24
|
def call
|
|
26
25
|
checks = CheckSet.call(@source, policy: @policy)
|
|
27
|
-
|
|
26
|
+
merged, analysis = CheckAnalyzer.call(
|
|
27
|
+
checks, provider: @llm_provider, source: @source, policy: @policy
|
|
28
|
+
)
|
|
29
|
+
CheckResultBuilder.call(merged, analysis, @policy)
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
private
|
|
@@ -1,33 +1,80 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
4
|
-
require_relative '
|
|
5
|
-
require_relative '
|
|
6
|
-
require_relative '
|
|
7
|
-
require_relative 'checks/secret_check'
|
|
8
|
-
require_relative 'checks/skipped_checks'
|
|
3
|
+
require_relative 'injection_detector'
|
|
4
|
+
require_relative 'pii/text_scrubber'
|
|
5
|
+
require_relative 'policy_scanner'
|
|
6
|
+
require_relative 'secret_scanner'
|
|
9
7
|
|
|
10
8
|
module Olyx
|
|
11
9
|
module Guardrails
|
|
12
10
|
# Coordinates independent deterministic checks for one normalized input.
|
|
13
11
|
class CheckSet
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
secret: Checks::SecretCheck,
|
|
18
|
-
policy: Checks::PolicyCheck
|
|
19
|
-
}.freeze
|
|
20
|
-
|
|
21
|
-
def self.call(source, policy:)
|
|
22
|
-
length = Checks::LengthCheck.call(source, policy)
|
|
23
|
-
content = length[:allowed] ? scan(source, policy) : Checks::SkippedChecks.call
|
|
12
|
+
def self.call(source, policy:, messages: nil)
|
|
13
|
+
length = length_check(source, policy)
|
|
14
|
+
content = length[:allowed] ? scan(source, policy, messages) : skipped_checks
|
|
24
15
|
content.merge(length: length)
|
|
25
16
|
end
|
|
26
17
|
|
|
27
|
-
def self.scan(source, policy)
|
|
28
|
-
|
|
18
|
+
def self.scan(source, policy, messages)
|
|
19
|
+
{
|
|
20
|
+
pii: pii_check(source, policy),
|
|
21
|
+
injection: injection_check(source, policy, messages),
|
|
22
|
+
secret: secret_check(source, policy),
|
|
23
|
+
policy: policy_check(source, policy)
|
|
24
|
+
}
|
|
29
25
|
end
|
|
30
|
-
|
|
26
|
+
|
|
27
|
+
def self.length_check(source, policy)
|
|
28
|
+
length = source.length
|
|
29
|
+
maximum = policy.max_input_length
|
|
30
|
+
{ type: 'length', allowed: length <= maximum, length: length, max_length: maximum }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.pii_check(source, policy)
|
|
34
|
+
detected = Pii::TextScrubber.detect?(source)
|
|
35
|
+
{ type: 'pii', allowed: !detected || !policy.block_pii?, detected: detected }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.injection_check(source, policy, messages)
|
|
39
|
+
scan = InjectionDetector.scan(messages || [{ 'role' => 'user', 'content' => source }])
|
|
40
|
+
attempt = scan[:injection_attempt]
|
|
41
|
+
{
|
|
42
|
+
type: 'injection',
|
|
43
|
+
allowed: !attempt || !policy.block_injections?,
|
|
44
|
+
injection_attempt: attempt,
|
|
45
|
+
patterns: scan[:patterns]
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.secret_check(source, policy)
|
|
50
|
+
scan = SecretScanner.scan(source, custom_patterns: policy.secret_patterns)
|
|
51
|
+
leaked = scan[:leaked]
|
|
52
|
+
{ type: 'secret', allowed: !leaked || !policy.block_secrets?, leaked: leaked, count: scan[:findings].size }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.policy_check(source, policy)
|
|
56
|
+
scan = PolicyScanner.scan(source, policy: policy)
|
|
57
|
+
findings = scan[:findings]
|
|
58
|
+
{
|
|
59
|
+
type: 'policy', allowed: !scan[:blocked], violated: scan[:violated],
|
|
60
|
+
count: findings.size, findings: findings
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.skipped_checks
|
|
65
|
+
{
|
|
66
|
+
pii: skipped('pii', detected: false),
|
|
67
|
+
injection: skipped('injection', injection_attempt: false, patterns: []),
|
|
68
|
+
secret: skipped('secret', leaked: false, count: 0),
|
|
69
|
+
policy: skipped('policy', violated: false, count: 0, findings: [])
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.skipped(type, **fields)
|
|
74
|
+
{ type: type, allowed: true, skipped: true, **fields }
|
|
75
|
+
end
|
|
76
|
+
private_class_method :injection_check, :length_check, :pii_check, :policy_check, :scan,
|
|
77
|
+
:secret_check, :skipped, :skipped_checks
|
|
31
78
|
end
|
|
32
79
|
end
|
|
33
80
|
end
|
|
@@ -19,8 +19,7 @@ module Olyx
|
|
|
19
19
|
/forget\s+(?:all\s+)?(?:your|previous)\s+instructions?/i,
|
|
20
20
|
/your\s+(?:new\s+)?(?:instructions?\s+are|rules?\s+are)/i,
|
|
21
21
|
/(?:override|bypass|ignore)\s+your\s+(?:instructions?|safety|restrictions?|filters?|rules?|training)/i,
|
|
22
|
-
/
|
|
23
|
-
/you\s+are\s+now\s+(?:a|an|free|DAN|unrestricted)/i,
|
|
22
|
+
/you\s+are\s+now\s+(?:an?\s+)?(?:free|DAN|unrestricted)(?:\s+AI)?/i,
|
|
24
23
|
/(?:jailbreak|dan\s+mode|developer\s+mode|god\s+mode|unrestricted\s+mode)/i,
|
|
25
24
|
/do\s+anything\s+now/i,
|
|
26
25
|
/you\s+have\s+no\s+(?:restrictions?|limits?|rules?|filters?)/i,
|
|
@@ -6,6 +6,8 @@ module Olyx
|
|
|
6
6
|
module Guardrails
|
|
7
7
|
# Normalizes and bounds responses from an untrusted optional LLM provider.
|
|
8
8
|
class LlmAnalysis
|
|
9
|
+
PROVIDER_ERROR = 'llm_provider failed'
|
|
10
|
+
|
|
9
11
|
def self.call(provider, text, context)
|
|
10
12
|
new(provider, text, context).call
|
|
11
13
|
end
|
|
@@ -18,8 +20,8 @@ module Olyx
|
|
|
18
20
|
|
|
19
21
|
def call
|
|
20
22
|
Llm::AnalysisPipeline.call(@provider.call(@text, @context))
|
|
21
|
-
rescue StandardError
|
|
22
|
-
{ error:
|
|
23
|
+
rescue StandardError
|
|
24
|
+
{ error: PROVIDER_ERROR }
|
|
23
25
|
end
|
|
24
26
|
end
|
|
25
27
|
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
4
|
-
require_relative '
|
|
3
|
+
require_relative 'check_analyzer'
|
|
4
|
+
require_relative 'check_result_builder'
|
|
5
|
+
require_relative 'check_set'
|
|
5
6
|
require_relative 'message_source'
|
|
6
7
|
require_relative 'policy'
|
|
7
8
|
require_relative 'validation'
|
|
@@ -16,8 +17,9 @@ module Olyx
|
|
|
16
17
|
|
|
17
18
|
Validation.callable_or_nil!(llm_provider, name: 'llm_provider')
|
|
18
19
|
source = MessageSource.call(messages)
|
|
19
|
-
checks =
|
|
20
|
-
|
|
20
|
+
checks = CheckSet.call(source, policy: policy, messages: messages)
|
|
21
|
+
merged, analysis = CheckAnalyzer.call(checks, provider: llm_provider, source: source, policy: policy)
|
|
22
|
+
CheckResultBuilder.call(merged, analysis, policy)
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
end
|
|
@@ -15,13 +15,27 @@ module Olyx
|
|
|
15
15
|
PatternCatalog::ENTRIES.reduce(text) { |output, entry| replace(output, entry) }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def detect?(text)
|
|
19
|
+
return false unless text.is_a?(String)
|
|
20
|
+
|
|
21
|
+
PatternCatalog::ENTRIES.any? { |entry| matches?(text, entry) }
|
|
22
|
+
end
|
|
23
|
+
|
|
18
24
|
def replace(text, entry)
|
|
19
25
|
pattern, replacement, validator = entry
|
|
20
26
|
return text.gsub(pattern, replacement) unless validator
|
|
21
27
|
|
|
22
28
|
text.gsub(pattern) { |match| validator.call(match) ? replacement : match }
|
|
23
29
|
end
|
|
24
|
-
|
|
30
|
+
|
|
31
|
+
def matches?(text, entry)
|
|
32
|
+
pattern, _, validator = entry
|
|
33
|
+
text.to_enum(:scan, pattern).any? do
|
|
34
|
+
match = Regexp.last_match[0]
|
|
35
|
+
!validator || validator.call(match)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
private_class_method :matches?, :replace
|
|
25
39
|
end
|
|
26
40
|
end
|
|
27
41
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'finding_order'
|
|
4
4
|
require_relative 'rule_matcher'
|
|
5
|
+
require_relative '../text/mapped_normalization'
|
|
5
6
|
|
|
6
7
|
module Olyx
|
|
7
8
|
module Guardrails
|
|
@@ -11,7 +12,12 @@ module Olyx
|
|
|
11
12
|
module_function
|
|
12
13
|
|
|
13
14
|
def call(source, rules)
|
|
14
|
-
|
|
15
|
+
return [] if rules.empty?
|
|
16
|
+
|
|
17
|
+
normalized = Text::MappedNormalization.new(source)
|
|
18
|
+
findings = rules.each_with_index.flat_map do |rule, index|
|
|
19
|
+
RuleMatcher.call(source, normalized, rule, index)
|
|
20
|
+
end
|
|
15
21
|
findings.uniq { |finding| FindingOrder.identity(finding) }.sort_by { |finding| FindingOrder.key(finding) }
|
|
16
22
|
end
|
|
17
23
|
end
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'pattern_matcher'
|
|
4
|
-
require_relative 'normalized_pattern_matcher'
|
|
5
|
-
|
|
6
3
|
module Olyx
|
|
7
4
|
module Guardrails
|
|
8
5
|
module PolicyComponents
|
|
@@ -12,17 +9,37 @@ module Olyx
|
|
|
12
9
|
|
|
13
10
|
module_function
|
|
14
11
|
|
|
15
|
-
def call(source, rule, index)
|
|
16
|
-
rule.patterns.flat_map { |pattern| matches(source, rule, index, pattern) }
|
|
12
|
+
def call(source, normalized, rule, index)
|
|
13
|
+
rule.patterns.flat_map { |pattern| matches(source, normalized, rule, index, pattern) }
|
|
17
14
|
rescue TIMEOUT_ERROR
|
|
18
15
|
raise ArgumentError, "policy rule #{rule.name.inspect} timed out"
|
|
19
16
|
end
|
|
20
17
|
|
|
21
|
-
def matches(source, rule, index, pattern)
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
def matches(source, normalized, rule, index, pattern)
|
|
19
|
+
scan(source, rule, index, pattern) + normalized_matches(source, normalized, rule, index, pattern)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def scan(source, rule, index, pattern)
|
|
23
|
+
source.to_enum(:scan, pattern).map do
|
|
24
|
+
match = Regexp.last_match
|
|
25
|
+
finding(rule, index, match[0].to_s, match.begin(0), match.end(0))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def normalized_matches(source, normalized, rule, index, pattern)
|
|
30
|
+
return [] unless normalized.changed?
|
|
31
|
+
|
|
32
|
+
normalized.text.to_enum(:scan, pattern).map do
|
|
33
|
+
match = Regexp.last_match
|
|
34
|
+
starting, ending = normalized.original_span(match.begin(0), match.end(0))
|
|
35
|
+
finding(rule, index, source[starting...ending], starting, ending)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def finding(rule, index, full, starting, ending)
|
|
40
|
+
{ rule: rule, rule_index: index, full: full, start: starting, end: ending }
|
|
24
41
|
end
|
|
25
|
-
private_class_method :matches
|
|
42
|
+
private_class_method :finding, :matches, :normalized_matches, :scan
|
|
26
43
|
end
|
|
27
44
|
end
|
|
28
45
|
end
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'policy/configuration_hash'
|
|
4
|
-
require_relative 'policy/
|
|
4
|
+
require_relative 'policy/name'
|
|
5
|
+
require_relative 'policy/rule_collection'
|
|
6
|
+
require_relative 'policy/secret_pattern_collection'
|
|
7
|
+
require_relative 'enum_value'
|
|
8
|
+
require_relative 'validation'
|
|
5
9
|
|
|
6
10
|
module Olyx # :nodoc:
|
|
7
11
|
module Guardrails
|
|
@@ -21,6 +25,27 @@ module Olyx # :nodoc:
|
|
|
21
25
|
#
|
|
22
26
|
# See docs/POLICIES.md for matching and replacement semantics.
|
|
23
27
|
class Policy
|
|
28
|
+
LLM_FAILURE_MODE = EnumValue.new(
|
|
29
|
+
allowed: %i[allow block raise],
|
|
30
|
+
error: 'policy llm_failure_mode must be allow, block, or raise'
|
|
31
|
+
)
|
|
32
|
+
private_constant :LLM_FAILURE_MODE
|
|
33
|
+
|
|
34
|
+
# Returns the policy's stable String identifier.
|
|
35
|
+
attr_reader :name
|
|
36
|
+
|
|
37
|
+
# Returns the maximum accepted input length in Ruby characters.
|
|
38
|
+
attr_reader :max_input_length
|
|
39
|
+
|
|
40
|
+
# Returns +:allow+, +:block+, or +:raise+.
|
|
41
|
+
attr_reader :llm_failure_mode
|
|
42
|
+
|
|
43
|
+
# Returns the frozen custom secret regular-expression source Strings.
|
|
44
|
+
attr_reader :secret_patterns
|
|
45
|
+
|
|
46
|
+
# Returns the frozen PolicyRule collection.
|
|
47
|
+
attr_reader :rules
|
|
48
|
+
|
|
24
49
|
# :call-seq:
|
|
25
50
|
# Policy.default -> Policy
|
|
26
51
|
#
|
|
@@ -67,37 +92,25 @@ module Olyx # :nodoc:
|
|
|
67
92
|
secret_patterns: [],
|
|
68
93
|
rules: []
|
|
69
94
|
)
|
|
70
|
-
@
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
95
|
+
@name = PolicyComponents::Name.call(name)
|
|
96
|
+
@max_input_length = Validation.non_negative_integer!(max_input_length, name: 'policy max_input_length')
|
|
97
|
+
@block_pii = Validation.boolean!(block_pii, name: 'policy block_pii')
|
|
98
|
+
@block_injections = Validation.boolean!(block_injections, name: 'policy block_injections')
|
|
99
|
+
@block_secrets = Validation.boolean!(block_secrets, name: 'policy block_secrets')
|
|
100
|
+
@llm_failure_mode = LLM_FAILURE_MODE.call(llm_failure_mode)
|
|
101
|
+
@secret_patterns = PolicyComponents::SecretPatternCollection.call(secret_patterns)
|
|
102
|
+
@rules = PolicyComponents::RuleCollection.call(rules)
|
|
75
103
|
freeze
|
|
76
104
|
end
|
|
77
105
|
|
|
78
|
-
# Returns the policy's stable String identifier.
|
|
79
|
-
def name = @configuration.identity.name
|
|
80
|
-
|
|
81
|
-
# Returns the maximum accepted input length in Ruby characters.
|
|
82
|
-
def max_input_length = @configuration.identity.max_input_length
|
|
83
|
-
|
|
84
|
-
# Returns +:allow+, +:block+, or +:raise+.
|
|
85
|
-
def llm_failure_mode = @configuration.restrictions.llm_failure_mode
|
|
86
|
-
|
|
87
|
-
# Returns the frozen custom secret regular-expression source Strings.
|
|
88
|
-
def secret_patterns = @configuration.restrictions.secret_patterns
|
|
89
|
-
|
|
90
|
-
# Returns the frozen PolicyRule collection.
|
|
91
|
-
def rules = @configuration.restrictions.rules
|
|
92
|
-
|
|
93
106
|
# Returns whether PII findings block a decision.
|
|
94
|
-
def block_pii? = @
|
|
107
|
+
def block_pii? = @block_pii
|
|
95
108
|
|
|
96
109
|
# Returns whether prompt-injection findings block a decision.
|
|
97
|
-
def block_injections? = @
|
|
110
|
+
def block_injections? = @block_injections
|
|
98
111
|
|
|
99
112
|
# Returns whether secret findings block a decision.
|
|
100
|
-
def block_secrets? = @
|
|
113
|
+
def block_secrets? = @block_secrets
|
|
101
114
|
end
|
|
102
115
|
end
|
|
103
116
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'policy_rule/
|
|
3
|
+
require_relative 'policy_rule/pattern_compiler'
|
|
4
|
+
require_relative 'validation'
|
|
4
5
|
|
|
5
6
|
module Olyx # :nodoc:
|
|
6
7
|
module Guardrails
|
|
@@ -10,6 +11,25 @@ module Olyx # :nodoc:
|
|
|
10
11
|
# records safe fingerprints and offsets; it does not expose the matched
|
|
11
12
|
# restricted text in public findings.
|
|
12
13
|
class PolicyRule
|
|
14
|
+
MATCH_MODES = %i[substring whole_word regexp].freeze
|
|
15
|
+
NAME_FORMAT = /\A[a-z][a-z0-9_.:-]*\z/i
|
|
16
|
+
private_constant :MATCH_MODES, :NAME_FORMAT
|
|
17
|
+
|
|
18
|
+
# Returns the normalized String rule identifier.
|
|
19
|
+
attr_reader :name
|
|
20
|
+
|
|
21
|
+
# Returns the optional description, or +nil+.
|
|
22
|
+
attr_reader :description
|
|
23
|
+
|
|
24
|
+
# Returns +:substring+, +:whole_word+, or +:regexp+.
|
|
25
|
+
attr_reader :match
|
|
26
|
+
|
|
27
|
+
# Returns the frozen compiled Regexp collection.
|
|
28
|
+
attr_reader :patterns
|
|
29
|
+
|
|
30
|
+
# Returns the safe replacement used by redaction.
|
|
31
|
+
attr_reader :replacement
|
|
32
|
+
|
|
13
33
|
# :call-seq:
|
|
14
34
|
# PolicyRule.new(name:, patterns: [], terms: [], match: :substring,
|
|
15
35
|
# block: true, description: nil, replacement: nil)
|
|
@@ -28,34 +48,51 @@ module Olyx # :nodoc:
|
|
|
28
48
|
# Invalid values and expressions that match empty text raise
|
|
29
49
|
# ArgumentError.
|
|
30
50
|
def initialize(name:, patterns: [], terms: [], match: :substring, block: true, description: nil, replacement: nil)
|
|
31
|
-
@
|
|
32
|
-
@
|
|
33
|
-
@
|
|
51
|
+
@name = validate_name(name)
|
|
52
|
+
@description = validate_description(description)
|
|
53
|
+
@match = validate_match(match)
|
|
54
|
+
@patterns = PolicyRuleComponents::PatternCompiler.call(patterns: patterns, terms: terms, match: @match)
|
|
55
|
+
@block = Validation.boolean!(block, name: 'policy rule block')
|
|
56
|
+
@replacement = validate_replacement(replacement || default_replacement)
|
|
34
57
|
freeze
|
|
35
58
|
end
|
|
36
59
|
|
|
37
|
-
# Returns
|
|
38
|
-
def
|
|
60
|
+
# Returns whether a match blocks a decision.
|
|
61
|
+
def block? = @block
|
|
39
62
|
|
|
40
|
-
|
|
41
|
-
def description = @identity.description
|
|
63
|
+
private
|
|
42
64
|
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
def default_replacement
|
|
66
|
+
"[RESTRICTED:#{@name.upcase}]"
|
|
67
|
+
end
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
|
|
69
|
+
def validate_name(value)
|
|
70
|
+
normalized = value.to_s
|
|
71
|
+
valid = (value.is_a?(String) || value.is_a?(Symbol)) && normalized.match?(NAME_FORMAT)
|
|
72
|
+
raise ArgumentError, 'policy rule name must be a String or Symbol identifier' unless valid
|
|
48
73
|
|
|
49
|
-
|
|
50
|
-
|
|
74
|
+
normalized.dup.freeze
|
|
75
|
+
end
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
def validate_match(value)
|
|
78
|
+
normalized = value.respond_to?(:to_sym) ? value.to_sym : value
|
|
79
|
+
return normalized if MATCH_MODES.include?(normalized)
|
|
54
80
|
|
|
55
|
-
|
|
81
|
+
raise ArgumentError, 'policy rule match must be substring, whole_word, or regexp'
|
|
82
|
+
end
|
|
56
83
|
|
|
57
|
-
def
|
|
58
|
-
|
|
84
|
+
def validate_description(value)
|
|
85
|
+
return nil if value.nil?
|
|
86
|
+
return value.dup.freeze if value.is_a?(String) && !value.strip.empty? && value.length <= 500
|
|
87
|
+
|
|
88
|
+
raise ArgumentError, 'policy rule description must be a String of 1..500 characters or nil'
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate_replacement(value)
|
|
92
|
+
valid = value.is_a?(String) && !value.empty? && value.length <= 100 && !value.match?(/[\r\n\t]/)
|
|
93
|
+
raise ArgumentError, 'policy rule replacement must be a single-line String of 1..100 characters' unless valid
|
|
94
|
+
|
|
95
|
+
value.dup.freeze
|
|
59
96
|
end
|
|
60
97
|
end
|
|
61
98
|
end
|
|
@@ -3,15 +3,8 @@
|
|
|
3
3
|
require 'active_support/notifications'
|
|
4
4
|
require 'forwardable'
|
|
5
5
|
require_relative '../guardrails'
|
|
6
|
-
require_relative 'rails/active_job_handler'
|
|
7
|
-
require_relative 'rails/action_cable'
|
|
8
6
|
require_relative 'rails/active_model_validator'
|
|
9
|
-
require_relative 'rails/controller'
|
|
10
|
-
require_relative 'rails/enforcer'
|
|
11
|
-
require_relative 'rails/graphql'
|
|
12
|
-
require_relative 'rails/job'
|
|
13
7
|
require_relative 'rails/runtime'
|
|
14
|
-
require_relative 'rails/upload'
|
|
15
8
|
|
|
16
9
|
module Olyx # :nodoc:
|
|
17
10
|
module Guardrails
|
|
@@ -29,6 +22,14 @@ module Olyx # :nodoc:
|
|
|
29
22
|
module Rails
|
|
30
23
|
extend SingleForwardable
|
|
31
24
|
|
|
25
|
+
autoload :ActiveJobHandler, File.join(__dir__, 'rails/active_job_handler')
|
|
26
|
+
autoload :ActionCable, File.join(__dir__, 'rails/action_cable')
|
|
27
|
+
autoload :Controller, File.join(__dir__, 'rails/controller')
|
|
28
|
+
autoload :Enforcer, File.join(__dir__, 'rails/enforcer')
|
|
29
|
+
autoload :GraphQL, File.join(__dir__, 'rails/graphql')
|
|
30
|
+
autoload :Job, File.join(__dir__, 'rails/job')
|
|
31
|
+
autoload :Upload, File.join(__dir__, 'rails/upload')
|
|
32
|
+
|
|
32
33
|
def self.runtime # :nodoc:
|
|
33
34
|
@runtime ||= Runtime.new
|
|
34
35
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../pii/text_scrubber'
|
|
4
|
+
|
|
3
5
|
module Olyx
|
|
4
6
|
module Guardrails
|
|
5
7
|
module Redaction
|
|
@@ -13,13 +15,12 @@ module Olyx
|
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def content(text, findings)
|
|
16
|
-
|
|
17
|
-
{ text: output || text, findings: findings }
|
|
18
|
+
{ text: text, findings: findings }
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def detection(source, secret_scan, policy_redaction)
|
|
21
22
|
{
|
|
22
|
-
pii_detected:
|
|
23
|
+
pii_detected: Pii::TextScrubber.detect?(source),
|
|
23
24
|
secret_leaked: secret_scan[:leaked],
|
|
24
25
|
policy_violated: policy_redaction[:violated],
|
|
25
26
|
policy_findings: policy_redaction[:findings]
|
|
@@ -9,10 +9,8 @@ module Olyx
|
|
|
9
9
|
module DeterministicScore
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
|
-
def call(checks
|
|
13
|
-
|
|
14
|
-
score += BLOCKED_RISK_WEIGHT if ordered_checks.any? { |check| !check[:allowed] }
|
|
15
|
-
score.clamp(0.0, 1.0).round(4)
|
|
12
|
+
def call(checks)
|
|
13
|
+
CheckWeights.call(checks).clamp(0.0, 1.0).round(4)
|
|
16
14
|
end
|
|
17
15
|
end
|
|
18
16
|
end
|