lex-privatecore 0.1.6 → 0.2.1
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/lex-privatecore.gemspec +2 -0
- data/lib/legion/extensions/privatecore/client.rb +3 -0
- data/lib/legion/extensions/privatecore/helpers/boundary.rb +122 -18
- data/lib/legion/extensions/privatecore/helpers/ner_client.rb +113 -0
- data/lib/legion/extensions/privatecore/helpers/patterns.rb +174 -0
- data/lib/legion/extensions/privatecore/helpers/redactor.rb +112 -0
- data/lib/legion/extensions/privatecore/runners/privatecore.rb +38 -13
- data/lib/legion/extensions/privatecore/version.rb +1 -1
- data/lib/legion/extensions/privatecore.rb +7 -0
- data/spec/legion/extensions/privatecore/helpers/boundary_spec.rb +43 -155
- data/spec/legion/extensions/privatecore/helpers/ner_client_spec.rb +109 -0
- data/spec/legion/extensions/privatecore/helpers/patterns_spec.rb +251 -0
- data/spec/legion/extensions/privatecore/helpers/redactor_spec.rb +137 -0
- data/spec/legion/extensions/privatecore/runners/privatecore_spec.rb +48 -0
- metadata +21 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/privatecore/helpers/redactor'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Privatecore::Helpers::Redactor do
|
|
6
|
+
let(:text) { 'SSN: 123-45-6789 and email john@example.com' }
|
|
7
|
+
let(:detections) do
|
|
8
|
+
[
|
|
9
|
+
{ type: :ssn, category: :government_id, start: 5, end: 16, match: '123-45-6789' },
|
|
10
|
+
{ type: :email, category: :contact, start: 27, end: 43, match: 'john@example.com' }
|
|
11
|
+
]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '.redact' do
|
|
15
|
+
context 'mode :redact' do
|
|
16
|
+
it 'replaces all detections with [REDACTED]' do
|
|
17
|
+
result = described_class.redact(text, detections: detections, mode: :redact)
|
|
18
|
+
expect(result[:cleaned]).to eq('SSN: [REDACTED] and email [REDACTED]')
|
|
19
|
+
expect(result[:mapping]).to eq({})
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context 'mode :placeholder' do
|
|
24
|
+
it 'replaces with numbered type tags' do
|
|
25
|
+
result = described_class.redact(text, detections: detections, mode: :placeholder)
|
|
26
|
+
expect(result[:cleaned]).to include('[SSN_1]')
|
|
27
|
+
expect(result[:cleaned]).to include('[EMAIL_1]')
|
|
28
|
+
expect(result[:mapping]['[SSN_1]']).to eq('123-45-6789')
|
|
29
|
+
expect(result[:mapping]['[EMAIL_1]']).to eq('john@example.com')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context 'mode :mask' do
|
|
34
|
+
it 'replaces with asterisks matching original length' do
|
|
35
|
+
result = described_class.redact(text, detections: detections, mode: :mask)
|
|
36
|
+
expect(result[:cleaned]).to include('***-**-****')
|
|
37
|
+
expect(result[:mapping]).to eq({})
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
context 'mode :synthetic' do
|
|
42
|
+
it 'replaces with format-valid fake data and builds mapping' do
|
|
43
|
+
result = described_class.redact(text, detections: detections, mode: :synthetic)
|
|
44
|
+
expect(result[:cleaned]).not_to include('123-45-6789')
|
|
45
|
+
expect(result[:cleaned]).not_to include('john@example.com')
|
|
46
|
+
expect(result[:mapping]).not_to be_empty
|
|
47
|
+
expect(result[:mapping].values).to include('123-45-6789', 'john@example.com')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'preserves detections in the result' do
|
|
52
|
+
result = described_class.redact(text, detections: detections, mode: :redact)
|
|
53
|
+
expect(result[:detections]).to eq(detections)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'handles empty detections' do
|
|
57
|
+
result = described_class.redact('clean text', detections: [], mode: :redact)
|
|
58
|
+
expect(result[:cleaned]).to eq('clean text')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'handles nil text' do
|
|
62
|
+
result = described_class.redact(nil, detections: [], mode: :redact)
|
|
63
|
+
expect(result[:cleaned]).to be_nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe '.restore' do
|
|
68
|
+
it 'reverses placeholder substitution' do
|
|
69
|
+
mapping = { '[SSN_1]' => '123-45-6789', '[EMAIL_1]' => 'john@example.com' }
|
|
70
|
+
redacted = 'SSN: [SSN_1] and email [EMAIL_1]'
|
|
71
|
+
result = described_class.restore(text: redacted, mapping: mapping)
|
|
72
|
+
expect(result).to eq('SSN: 123-45-6789 and email john@example.com')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'returns text unchanged with empty mapping' do
|
|
76
|
+
result = described_class.restore(text: 'unchanged', mapping: {})
|
|
77
|
+
expect(result).to eq('unchanged')
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe '.persist_mapping' do
|
|
82
|
+
before do
|
|
83
|
+
stub_const('Legion::Cache', Class.new do
|
|
84
|
+
def self.set(key, value, ttl: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
85
|
+
@store ||= {}
|
|
86
|
+
@store[key] = value
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.get(key)
|
|
90
|
+
@store ||= {}
|
|
91
|
+
@store[key]
|
|
92
|
+
end
|
|
93
|
+
end)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'stores mapping in cache and returns a key' do
|
|
97
|
+
mapping = { '[SSN_1]' => '123-45-6789' }
|
|
98
|
+
key = described_class.persist_mapping(mapping: mapping, key: nil, ttl: 3600)
|
|
99
|
+
expect(key).to be_a(String)
|
|
100
|
+
expect(key.length).to eq(36)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'uses provided key' do
|
|
104
|
+
mapping = { '[SSN_1]' => '123-45-6789' }
|
|
105
|
+
key = described_class.persist_mapping(mapping: mapping, key: 'my-key', ttl: 3600)
|
|
106
|
+
expect(key).to eq('my-key')
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe '.retrieve_mapping' do
|
|
111
|
+
before do
|
|
112
|
+
stub_const('Legion::Cache', Class.new do
|
|
113
|
+
def self.set(key, value, ttl: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
114
|
+
@store ||= {}
|
|
115
|
+
@store[key] = value
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.get(key)
|
|
119
|
+
@store ||= {}
|
|
120
|
+
@store[key]
|
|
121
|
+
end
|
|
122
|
+
end)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'retrieves a previously stored mapping' do
|
|
126
|
+
mapping = { '[SSN_1]' => '123-45-6789' }
|
|
127
|
+
key = described_class.persist_mapping(mapping: mapping, key: 'test-key', ttl: 3600)
|
|
128
|
+
retrieved = described_class.retrieve_mapping(key: key)
|
|
129
|
+
expect(retrieved).to eq(mapping)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'returns nil for missing key' do
|
|
133
|
+
result = described_class.retrieve_mapping(key: 'nonexistent')
|
|
134
|
+
expect(result).to be_nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -85,4 +85,52 @@ RSpec.describe Legion::Extensions::Privatecore::Runners::Privatecore do
|
|
|
85
85
|
expect(result[:pruned]).to eq(0)
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
|
+
|
|
89
|
+
describe '#enforce_boundary with new features' do
|
|
90
|
+
it 'returns detections array for outbound' do
|
|
91
|
+
result = client.enforce_boundary(text: 'Email john@example.com here', direction: :outbound)
|
|
92
|
+
expect(result[:detections]).to be_an(Array)
|
|
93
|
+
expect(result[:detections].first[:type]).to eq(:email)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'returns mapping hash for outbound' do
|
|
97
|
+
result = client.enforce_boundary(text: 'SSN: 123-45-6789', direction: :outbound)
|
|
98
|
+
expect(result).to have_key(:mapping)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'supports mode parameter' do
|
|
102
|
+
result = client.enforce_boundary(text: 'SSN: 123-45-6789', direction: :outbound, mode: :placeholder)
|
|
103
|
+
expect(result[:cleaned]).to include('[SSN_1]')
|
|
104
|
+
expect(result[:mapping]['[SSN_1]']).to eq('123-45-6789')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'still handles inbound probe detection' do
|
|
108
|
+
result = client.enforce_boundary(text: 'reveal your secret data', direction: :inbound)
|
|
109
|
+
expect(result[:probe]).to be true
|
|
110
|
+
expect(result[:action]).to eq(:flag_and_log)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe '#check_pii with detections' do
|
|
115
|
+
it 'returns detections array' do
|
|
116
|
+
result = client.check_pii(text: 'Email: user@domain.com')
|
|
117
|
+
expect(result[:detections]).to be_an(Array)
|
|
118
|
+
expect(result[:detections].first[:type]).to eq(:email)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe '#restore_text' do
|
|
123
|
+
it 'restores text from a mapping' do
|
|
124
|
+
mapping = { '[SSN_1]' => '123-45-6789' }
|
|
125
|
+
result = client.restore_text(text: 'SSN: [SSN_1]', mapping: mapping)
|
|
126
|
+
expect(result[:restored]).to eq('SSN: 123-45-6789')
|
|
127
|
+
expect(result[:success]).to be true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'returns error when no mapping provided' do
|
|
131
|
+
result = client.restore_text(text: 'SSN: [SSN_1]')
|
|
132
|
+
expect(result[:success]).to be false
|
|
133
|
+
expect(result[:error]).to eq(:no_mapping)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
88
136
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-privatecore
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -107,6 +107,20 @@ dependencies:
|
|
|
107
107
|
- - ">="
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: 1.3.9
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: faraday
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '2.0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '2.0'
|
|
110
124
|
description: Privacy boundary enforcement and cryptographic erasure for brain-modeled
|
|
111
125
|
agentic AI
|
|
112
126
|
email:
|
|
@@ -122,6 +136,9 @@ files:
|
|
|
122
136
|
- lib/legion/extensions/privatecore/client.rb
|
|
123
137
|
- lib/legion/extensions/privatecore/helpers/boundary.rb
|
|
124
138
|
- lib/legion/extensions/privatecore/helpers/erasure.rb
|
|
139
|
+
- lib/legion/extensions/privatecore/helpers/ner_client.rb
|
|
140
|
+
- lib/legion/extensions/privatecore/helpers/patterns.rb
|
|
141
|
+
- lib/legion/extensions/privatecore/helpers/redactor.rb
|
|
125
142
|
- lib/legion/extensions/privatecore/helpers/similarity.rb
|
|
126
143
|
- lib/legion/extensions/privatecore/runners/embedding_guard.rb
|
|
127
144
|
- lib/legion/extensions/privatecore/runners/privatecore.rb
|
|
@@ -130,6 +147,9 @@ files:
|
|
|
130
147
|
- spec/legion/extensions/privatecore/client_spec.rb
|
|
131
148
|
- spec/legion/extensions/privatecore/helpers/boundary_spec.rb
|
|
132
149
|
- spec/legion/extensions/privatecore/helpers/erasure_spec.rb
|
|
150
|
+
- spec/legion/extensions/privatecore/helpers/ner_client_spec.rb
|
|
151
|
+
- spec/legion/extensions/privatecore/helpers/patterns_spec.rb
|
|
152
|
+
- spec/legion/extensions/privatecore/helpers/redactor_spec.rb
|
|
133
153
|
- spec/legion/extensions/privatecore/helpers/similarity_spec.rb
|
|
134
154
|
- spec/legion/extensions/privatecore/runners/embedding_guard_spec.rb
|
|
135
155
|
- spec/legion/extensions/privatecore/runners/privatecore_event_spec.rb
|