data_redactor 0.14.0-x86_64-linux → 0.16.0-x86_64-linux
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 +66 -2
- data/README.md +65 -18
- data/lib/data_redactor/3.0/data_redactor.so +0 -0
- data/lib/data_redactor/3.1/data_redactor.so +0 -0
- data/lib/data_redactor/3.2/data_redactor.so +0 -0
- data/lib/data_redactor/3.3/data_redactor.so +0 -0
- data/lib/data_redactor/3.4/data_redactor.so +0 -0
- data/lib/data_redactor/4.0/data_redactor.so +0 -0
- data/lib/data_redactor/refinements.rb +69 -0
- data/lib/data_redactor/version.rb +1 -1
- data/lib/data_redactor.rb +22 -14
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 623b5727115b0b9ecc112585feeb1d3427222f4b6b9b5f1bd4c5298b5c1ec414
|
|
4
|
+
data.tar.gz: 7d7a4e52d8ecdfc9caa03db73c460146b4ab4041e05bd18fa2ad46f3cb07317d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8616fec36dccc41a9a4171f21f097a580e956e06666caefa76abb74b7f1ab2280982717989f21b46653c173fea85a0bdb93a6253f97c3d43dadb62eb6b92c656
|
|
7
|
+
data.tar.gz: a2facae8ff30570cc33c4e7c76401ff82273e94a9c4a155aee44d16ce8ebf25cd0ae41b44061ceb16788a8babad3705f2ea49664f0d836c30c433b57fae34ccc
|
data/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.
|
|
10
|
+
## [0.16.0] - 2026-06-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Opt-in `#redact` refinements.** `require "data_redactor/refinements"` then
|
|
14
|
+
`using DataRedactor::Refinements` adds `#redact` to `String` (→
|
|
15
|
+
`DataRedactor.redact`) and to `Hash`/`Array` (→ `DataRedactor.redact_deep`), e.g.
|
|
16
|
+
`"email a@b.com".redact` and `chat.ask(user_input.redact)`. Refinements are
|
|
17
|
+
lexically scoped, so they never pollute the core classes globally — apps that
|
|
18
|
+
don't opt in are unaffected and there is no collision risk. Forwards
|
|
19
|
+
`only:`/`except:`/`placeholder:`; never mutates the receiver.
|
|
20
|
+
`DataRedactor.redact` remains the primary API.
|
|
21
|
+
- **Length-aware placeholder modes.** `placeholder: :length` replaces each match
|
|
22
|
+
with `[REDACTED:N]` and `placeholder: :tagged_length` with `[REDACTED:TAGNAME:N]`,
|
|
23
|
+
where `N` is the **byte length** of the redacted value. Readers can gauge what
|
|
24
|
+
was there without seeing it. Both compose with `only:`/`except:` and are
|
|
25
|
+
forwarded by `redact_deep`, `redact_json`, and the integrations. Additive —
|
|
26
|
+
two new values for the existing `placeholder:` keyword; no behaviour changes.
|
|
27
|
+
- **CI: ASan/UBSan memory-safety gate.** New job builds the matcher engine
|
|
28
|
+
standalone under `-fsanitize=address,undefined` and drives it over an
|
|
29
|
+
adversarial corpus + seeded fuzz loop (catches the `OP_EOL`-class OOB read).
|
|
30
|
+
- **CI: throughput-trend history + PR comment.** New job records the C/pure-Ruby
|
|
31
|
+
ratio over time (history in `actions/cache`, no gh-pages), posts a sticky PR
|
|
32
|
+
comment comparing each run to the previous point, and fails on a >10% drop.
|
|
33
|
+
|
|
34
|
+
## [0.15.0] - 2026-06-17
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- **Overlap resolution is now longest-match-wins** (was earlier-index-wins). When
|
|
38
|
+
two patterns match overlapping spans, the engine keeps the **longer** span;
|
|
39
|
+
equal-length ties go to the lower pattern index (preserving prior behaviour for
|
|
40
|
+
same-length matches). The previous "earliest pattern by index wins any region it
|
|
41
|
+
can match" semantic was an accidental by-product of sequential per-pattern
|
|
42
|
+
rewriting, and it could leave a secret **partly unredacted** — e.g.
|
|
43
|
+
`AKIA…EXAMPLE` followed by 20 more alphanumeric bytes used to redact only the
|
|
44
|
+
20-char access-key prefix and leak the trailing 20 bytes; it now redacts the full
|
|
45
|
+
40-char secret. The public API (`redact`, `scan`) is unchanged; `scan` may report
|
|
46
|
+
one longer match where it previously reported several shorter overlapping ones.
|
|
47
|
+
Aligns with Onigmo/PCRE/RE2/Hyperscan semantics. Resolver only — no measurable
|
|
48
|
+
throughput change (still ~2.4× over pure-Ruby on the 1 MB log).
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
- **CI throughput regression gate** (`throughput-gate` job). Runs
|
|
52
|
+
`benchmark/ci_throughput_gate.rb`, which gates on the ratio of the C engine to
|
|
53
|
+
a pure-Ruby gsub loop over the same patterns (the ratio cancels CI-runner
|
|
54
|
+
speed variance, unlike absolute MB/s). Loose floor (1.5×; known result
|
|
55
|
+
~2.25×), informational throughput output, plus a correctness guard so an
|
|
56
|
+
engine that redacts less cannot pass as "faster". Repo/CI only — not packaged.
|
|
57
|
+
|
|
58
|
+
## [0.14.1] - 2026-06-17
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
- **Bounded the greedy tails of seven built-in token patterns** (`jwt`,
|
|
62
|
+
`grafana_api_token`, `ssh_public_key`, `bearer_token`, `anthropic_api_key`,
|
|
63
|
+
`openai_project_api_key`, `sendgrid_api_key`). Open-ended quantifiers (`+` and
|
|
64
|
+
`{n,}`) are capped at the POSIX `RE_DUP_MAX` of 255 (`{n,255}`), matching the
|
|
65
|
+
existing `hashicorp_vault_batch_token` precedent. A token is unusable once its
|
|
66
|
+
front is redacted, so a bounded prefix is sufficient to neutralize it. This
|
|
67
|
+
restores a finite `max_len` for these patterns (re-enabling the engine's
|
|
68
|
+
literal back-up skip) and removes a theoretical O(N²) worst case where a
|
|
69
|
+
crafted prefix plus a megabyte of matching characters forces a long greedy
|
|
70
|
+
scan. Tokens longer than 255 characters are still neutralized — only a
|
|
71
|
+
cryptographically-dead tail may remain.
|
|
11
72
|
|
|
12
73
|
### Added
|
|
13
74
|
- **Key-name-anchored secret redaction** (`:credentials`). A new pattern tier
|
|
@@ -275,7 +336,10 @@ features as 0.7.1 plus the pipeline fix.
|
|
|
275
336
|
- `DataRedactor.redact(text)` module function returning the input with every match replaced by `[REDACTED]`.
|
|
276
337
|
- RSpec suite with one example per pattern.
|
|
277
338
|
|
|
278
|
-
[Unreleased]: https://github.com/danielefrisanco/data_redactor/compare/v0.
|
|
339
|
+
[Unreleased]: https://github.com/danielefrisanco/data_redactor/compare/v0.16.0...HEAD
|
|
340
|
+
[0.16.0]: https://github.com/danielefrisanco/data_redactor/compare/v0.15.0...v0.16.0
|
|
341
|
+
[0.15.0]: https://github.com/danielefrisanco/data_redactor/compare/v0.14.1...v0.15.0
|
|
342
|
+
[0.14.1]: https://github.com/danielefrisanco/data_redactor/compare/v0.14.0...v0.14.1
|
|
279
343
|
[0.14.0]: https://github.com/danielefrisanco/data_redactor/compare/v0.13.0...v0.14.0
|
|
280
344
|
[0.13.0]: https://github.com/danielefrisanco/data_redactor/compare/v0.11.0...v0.13.0
|
|
281
345
|
[0.11.0]: https://github.com/danielefrisanco/data_redactor/compare/v0.10.1...v0.11.0
|
data/README.md
CHANGED
|
@@ -6,16 +6,22 @@
|
|
|
6
6
|
|
|
7
7
|
A Ruby gem with a C extension for high-performance regex-based redaction of sensitive data from strings.
|
|
8
8
|
|
|
9
|
+
> 📄 The engineering behind the v19 matching engine is written up as an experience
|
|
10
|
+
> report, *"The Fastest Engine Is Not the Shippable Engine: Replacing a Regex Engine
|
|
11
|
+
> for Data Redaction Under Production Constraints,"* currently under review at
|
|
12
|
+
> *Software: Practice and Experience* (Manuscript ID 7985366). Source and the
|
|
13
|
+
> reproducibility bundle are in [`paper/`](paper/).
|
|
14
|
+
|
|
9
15
|
## What it does
|
|
10
16
|
|
|
11
17
|
DataRedactor scans text for sensitive data — API keys and cloud secrets, IBANs,
|
|
12
18
|
credit cards, national IDs, emails, phone numbers, IPs, and more — and replaces
|
|
13
19
|
each match with a placeholder. The scanning runs in a C extension backed by a
|
|
14
20
|
zero-dependency Thompson NFA → lazy-DFA multi-pattern engine (v19) that scans
|
|
15
|
-
|
|
21
|
+
every built-in pattern in a single pass — 2–2.5× faster than pure-Ruby `gsub`
|
|
16
22
|
on large payloads, with no external library dependencies.
|
|
17
23
|
|
|
18
|
-
It ships **
|
|
24
|
+
It ships **89 built-in patterns** across 15+ countries, grouped into tags
|
|
19
25
|
(`:credentials`, `:financial`, `:contact`, ...) so you can redact only what you
|
|
20
26
|
care about. Beyond plain strings it can walk nested Hashes, Arrays, and JSON,
|
|
21
27
|
audit a payload without mutating it (`scan`), and plug into Logger, Rails, and
|
|
@@ -103,9 +109,18 @@ DataRedactor.redact(text, placeholder: :hash)
|
|
|
103
109
|
# "user@example.com" → "[CONTACT_3d7a]"
|
|
104
110
|
# "user@example.com" → "[CONTACT_3d7a]" (same every time)
|
|
105
111
|
# "other@example.com" → "[CONTACT_91fc]" (different value, different hash)
|
|
112
|
+
|
|
113
|
+
# Length — embeds the byte length of the redacted value, so readers can
|
|
114
|
+
# gauge what was there without seeing it.
|
|
115
|
+
DataRedactor.redact(text, placeholder: :length)
|
|
116
|
+
# "user@example.com" → "[REDACTED:16]"
|
|
117
|
+
|
|
118
|
+
# Tagged length — tag name plus byte length.
|
|
119
|
+
DataRedactor.redact(text, placeholder: :tagged_length)
|
|
120
|
+
# "user@example.com" → "[REDACTED:CONTACT:16]"
|
|
106
121
|
```
|
|
107
122
|
|
|
108
|
-
All
|
|
123
|
+
All modes compose with `only:` and `except:`:
|
|
109
124
|
|
|
110
125
|
```ruby
|
|
111
126
|
DataRedactor.redact(text, only: :contact, placeholder: :tagged)
|
|
@@ -165,6 +180,24 @@ safe_json = DataRedactor.redact_json('{"email":"alice@example.com","count":3}')
|
|
|
165
180
|
DataRedactor.redact_json("not json") # => JSON::ParserError
|
|
166
181
|
```
|
|
167
182
|
|
|
183
|
+
### `#redact` refinements (opt-in)
|
|
184
|
+
|
|
185
|
+
Prefer `"text".redact` over `DataRedactor.redact("text")`? Opt into the refinement. It adds `#redact` to `String` (via `redact`) and to `Hash`/`Array` (via `redact_deep`) **only in the files that `using` it** — refinements are lexically scoped, so the core classes are never monkey-patched globally and there is no collision risk for apps that don't opt in. `DataRedactor.redact` remains the primary API.
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
require "data_redactor/refinements"
|
|
189
|
+
using DataRedactor::Refinements
|
|
190
|
+
|
|
191
|
+
"email alice@example.com".redact # => "email [REDACTED]"
|
|
192
|
+
{ token: "AKIAIOSFODNN7EXAMPLE" }.redact # => { token: "[REDACTED]" }
|
|
193
|
+
["a@b.com", 3].redact # => ["[REDACTED]", 3]
|
|
194
|
+
|
|
195
|
+
# Handy right before sending text to an LLM:
|
|
196
|
+
chat.ask(user_input.redact)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`#redact` forwards `only:`/`except:`/`placeholder:` and never mutates the receiver. Without `using DataRedactor::Refinements` in the current file, `#redact` is not defined.
|
|
200
|
+
|
|
168
201
|
### Custom patterns
|
|
169
202
|
|
|
170
203
|
Teams often have internal IDs that the gem can't ship. Register them at boot — or at runtime from any thread (registration is thread-safe, see [Thread safety](#thread-safety)):
|
|
@@ -309,7 +342,25 @@ safe_response = DataRedactor::Integrations::OpenAI.redact_response(response)
|
|
|
309
342
|
|
|
310
343
|
`content` may be a plain String or an array of content blocks/parts (`{ type: "text", text: "..." }`) — only the `text` of `text` blocks is redacted; image and other block types pass through untouched. For Claude, a top-level `system:` String is also redacted; for OpenAI, a `{ role: "system" }` message in the array is redacted like any other. Pass a bare `messages` array or the whole request Hash (with a `messages` key) — either works.
|
|
311
344
|
|
|
312
|
-
|
|
345
|
+
### RubyLLM
|
|
346
|
+
|
|
347
|
+
[RubyLLM](https://rubyllm.com) is a unified Ruby client for every major LLM provider — and a perfect match for `data_redactor`: anything you send to a model is exactly the kind of free text that leaks secrets and PII. Because RubyLLM takes plain strings, you can scrub them with `DataRedactor.redact` before they leave the process — no extra integration required:
|
|
348
|
+
|
|
349
|
+
```ruby
|
|
350
|
+
require "ruby_llm"
|
|
351
|
+
require "data_redactor"
|
|
352
|
+
|
|
353
|
+
chat = RubyLLM.chat(model: "claude-opus-4-8")
|
|
354
|
+
chat.with_instructions(DataRedactor.redact("You are a support agent for ACME Corp."))
|
|
355
|
+
|
|
356
|
+
user_input = "My card is 4111 1111 1111 1111 and my email is alice@example.com"
|
|
357
|
+
chat.ask(DataRedactor.redact(user_input))
|
|
358
|
+
# the model receives: "My card is [REDACTED] and my email is [REDACTED]"
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Wrap each prompt (and any `with_instructions` system prompt) in `DataRedactor.redact` before passing it to `ask`. This is a per-call step you opt into — RubyLLM does not yet expose a request hook for automatic, transparent redaction of every outbound call ([crmne/ruby_llm#765](https://github.com/crmne/ruby_llm/issues/765) tracks the connection-middleware hook that would enable it).
|
|
362
|
+
|
|
363
|
+
## Detected patterns (89 total)
|
|
313
364
|
|
|
314
365
|
The table below is a representative sample. Use `DataRedactor.pattern_names` for the canonical, machine-readable list — it stays in sync with the C extension automatically.
|
|
315
366
|
|
|
@@ -435,7 +486,7 @@ redactor/
|
|
|
435
486
|
│ ├── README.md # How to run, what each script measures
|
|
436
487
|
│ ├── support/corpus.rb # Shared payload builders + pure-Ruby baseline redactor
|
|
437
488
|
│ ├── throughput.rb # MB/s on representative payloads
|
|
438
|
-
│ ├── vs_pure_ruby.rb # C extension vs pure-Ruby gsub (same
|
|
489
|
+
│ ├── vs_pure_ruby.rb # C extension vs pure-Ruby gsub (same patterns)
|
|
439
490
|
│ ├── scaling.rb # Runtime vs input size 1KB → 50MB
|
|
440
491
|
│ └── per_pattern.rb # Per-pattern scan cost
|
|
441
492
|
└── docs/ # Design and execution docs for future work
|
|
@@ -523,7 +574,7 @@ different angles. They are **not** packaged with the gem.
|
|
|
523
574
|
```bash
|
|
524
575
|
bundle install # pulls benchmark-ips, benchmark-memory (dev deps)
|
|
525
576
|
bundle exec rake compile
|
|
526
|
-
bundle exec ruby benchmark/vs_pure_ruby.rb # head-to-head vs pure-Ruby gsub, same
|
|
577
|
+
bundle exec ruby benchmark/vs_pure_ruby.rb # head-to-head vs pure-Ruby gsub, same patterns
|
|
527
578
|
bundle exec ruby benchmark/throughput.rb # MB/s on a log line, JSON, 1MB and 10MB log files
|
|
528
579
|
bundle exec ruby benchmark/scaling.rb # runtime vs input size (1KB → 50MB), confirms linear scaling
|
|
529
580
|
bundle exec ruby benchmark/per_pattern.rb # per-pattern scan cost over a 1MB payload
|
|
@@ -535,11 +586,8 @@ C engine uses, via `DataRedactor::BUILTIN_PATTERN_SOURCES`).
|
|
|
535
586
|
|
|
536
587
|
### Performance (0.10.0 — v19 multi-pattern engine)
|
|
537
588
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
with two selective-merge passes (pure-digit group + IBAN union) that further
|
|
541
|
-
reduce work for the most common pattern classes. Custom patterns (`add_pattern`)
|
|
542
|
-
still use the glibc path (required for correct UTF-8 diacritic matching).
|
|
589
|
+
Measured on the v19 engine ([How it works](#how-it-works)) vs a pure-Ruby `gsub`
|
|
590
|
+
loop over the same patterns:
|
|
543
591
|
|
|
544
592
|
| Payload | v19 engine (0.10.0) | Pure-Ruby `gsub` | Ratio |
|
|
545
593
|
|-----------------------|---------------------|------------------|-----------------|
|
|
@@ -576,14 +624,14 @@ machine-dependent, but the flat curve is not.
|
|
|
576
624
|
|
|
577
625
|
## How it works
|
|
578
626
|
|
|
579
|
-
1. At load time, `
|
|
580
|
-
2. `DataRedactor.redact(text)`
|
|
581
|
-
3.
|
|
582
|
-
4.
|
|
627
|
+
1. At load time, `mm_init()` compiles every built-in pattern from a Thompson NFA into bytecode, lazily building each pattern's DFA on first use (interned and cached). Boundary-wrapped patterns are expanded with the word-boundary group before compilation.
|
|
628
|
+
2. `DataRedactor.redact(text)` / `scan(text)` hand the input to the v19 engine, which scans it **once** and emits `(pattern_id, start, length)` events for every enabled pattern. Two selective-merge passes (a pure-digit group and an IBAN union) collapse the most common pattern classes into shared scans. The single pass over the original buffer is what makes the engine O(N).
|
|
629
|
+
3. The raw events are resolved by `mm_resolve` under the **longest-match-wins** policy: overlapping spans are reduced to a non-overlapping set keeping the longest match at each position, with the lower pattern index breaking equal-length ties.
|
|
630
|
+
4. `redact` rewrites the surviving spans to placeholders in one buffer build (preserving the boundary characters of boundary-wrapped matches); `scan` returns the event list with byte offsets into the original string. Custom patterns (`add_pattern`) run on the glibc `regexec` path afterward — required for correct UTF-8 diacritic matching.
|
|
583
631
|
|
|
584
632
|
## Memory management
|
|
585
633
|
|
|
586
|
-
All C-side buffers are heap-allocated
|
|
634
|
+
All C-side working buffers are heap-allocated and freed before the call returns; the only Ruby-managed allocation is the final result `String`. No Ruby objects are created mid-scan, so GC cannot collect anything out from under the C code. Per-thread engine scratch (NFA state, lazy-DFA cache) is freed automatically when the thread exits — see [Thread safety](#thread-safety).
|
|
587
635
|
|
|
588
636
|
## Thread safety
|
|
589
637
|
|
|
@@ -601,7 +649,6 @@ Released under the [MIT License](LICENSE).
|
|
|
601
649
|
|
|
602
650
|
## Known limitations
|
|
603
651
|
|
|
604
|
-
- **Pattern ordering matters** — patterns run sequentially. An early broad pattern (e.g. the 9-digit passport) may consume digits that a later pattern (e.g. credit card) depends on. Boundary wrapping mitigates this for pure-digit patterns.
|
|
605
652
|
- **AWS Secret Key (pattern 1)** — 40 consecutive base64 characters is a broad match. It can produce false positives in base64-encoded content such as embedded images or binary blobs.
|
|
606
653
|
- **Duplicate digit patterns** — several national ID formats share the same digit-length (11 digits: PESEL, Norwegian Fødselsnummer, Belgian National Number). They are kept as separate slots for clarity but the practical effect is that any 11-digit boundary-delimited number will be redacted.
|
|
607
|
-
- **
|
|
654
|
+
- **Overlap resolution is longest-match-wins** — when two patterns match overlapping spans the engine keeps the longer span; equal-length ties go to the lower pattern index. This favours redacting *more* when uncertain (a 40-char secret is redacted whole rather than leaking the bytes past a shorter prefix match). When two secrets abut with **no separator** between them, a boundary-wrapped pattern can fail to match because the original buffer has no word boundary where one token meets the next, leaving the abutting token unredacted. This is rare in real text (secrets are almost always separator-delimited).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "data_redactor"
|
|
4
|
+
|
|
5
|
+
module DataRedactor
|
|
6
|
+
# Opt-in refinements that add a `#redact` method to `String`, `Hash`, and
|
|
7
|
+
# `Array` as sugar over {DataRedactor.redact} / {DataRedactor.redact_deep}.
|
|
8
|
+
#
|
|
9
|
+
# Refinements are lexically scoped: `#redact` exists only in files that
|
|
10
|
+
# `using DataRedactor::Refinements`, so loading this file never pollutes the
|
|
11
|
+
# core classes globally. Apps that don't opt in are unaffected, and there is
|
|
12
|
+
# no collision risk with other libraries' `String#redact`.
|
|
13
|
+
#
|
|
14
|
+
# `DataRedactor.redact` remains the primary API; this is convenience only.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# require "data_redactor/refinements"
|
|
18
|
+
# using DataRedactor::Refinements
|
|
19
|
+
#
|
|
20
|
+
# "email alice@example.com".redact #=> "email [REDACTED]"
|
|
21
|
+
# { token: "AKIAIOSFODNN7EXAMPLE" }.redact #=> { token: "[REDACTED]" }
|
|
22
|
+
# chat.ask(user_input.redact) # scrub before sending to an LLM
|
|
23
|
+
module Refinements
|
|
24
|
+
refine String do
|
|
25
|
+
# Redact this String via {DataRedactor.redact}. Returns a new String; the
|
|
26
|
+
# receiver is not mutated.
|
|
27
|
+
#
|
|
28
|
+
# @param only forwarded to {DataRedactor.redact}
|
|
29
|
+
# @param except forwarded to {DataRedactor.redact}
|
|
30
|
+
# @param placeholder forwarded to {DataRedactor.redact}
|
|
31
|
+
# @return [String] the redacted copy.
|
|
32
|
+
# @example
|
|
33
|
+
# "ssn 123-45-6789".redact #=> "ssn [REDACTED]"
|
|
34
|
+
def redact(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT)
|
|
35
|
+
DataRedactor.redact(self, only: only, except: except, placeholder: placeholder)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
refine Hash do
|
|
40
|
+
# Deep-redact this Hash's String values via {DataRedactor.redact_deep}.
|
|
41
|
+
# Returns a deep copy; the receiver is not mutated and keys are untouched.
|
|
42
|
+
#
|
|
43
|
+
# @param only forwarded to {DataRedactor.redact_deep}
|
|
44
|
+
# @param except forwarded to {DataRedactor.redact_deep}
|
|
45
|
+
# @param placeholder forwarded to {DataRedactor.redact_deep}
|
|
46
|
+
# @return [Hash] a deep copy with String leaves redacted.
|
|
47
|
+
# @example
|
|
48
|
+
# { email: "a@b.com" }.redact #=> { email: "[REDACTED]" }
|
|
49
|
+
def redact(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT)
|
|
50
|
+
DataRedactor.redact_deep(self, only: only, except: except, placeholder: placeholder)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
refine Array do
|
|
55
|
+
# Deep-redact this Array's String elements via {DataRedactor.redact_deep}.
|
|
56
|
+
# Returns a deep copy; the receiver is not mutated.
|
|
57
|
+
#
|
|
58
|
+
# @param only forwarded to {DataRedactor.redact_deep}
|
|
59
|
+
# @param except forwarded to {DataRedactor.redact_deep}
|
|
60
|
+
# @param placeholder forwarded to {DataRedactor.redact_deep}
|
|
61
|
+
# @return [Array] a deep copy with String leaves redacted.
|
|
62
|
+
# @example
|
|
63
|
+
# ["a@b.com", 3].redact #=> ["[REDACTED]", 3]
|
|
64
|
+
def redact(only: nil, except: nil, placeholder: DataRedactor::PLACEHOLDER_DEFAULT)
|
|
65
|
+
DataRedactor.redact_deep(self, only: only, except: except, placeholder: placeholder)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/data_redactor.rb
CHANGED
|
@@ -23,8 +23,10 @@ require_relative "data_redactor/name_pattern"
|
|
|
23
23
|
#
|
|
24
24
|
# @example Custom placeholder
|
|
25
25
|
# DataRedactor.redact(text, placeholder: "***")
|
|
26
|
-
# DataRedactor.redact(text, placeholder: :tagged)
|
|
27
|
-
# DataRedactor.redact(text, placeholder: :hash)
|
|
26
|
+
# DataRedactor.redact(text, placeholder: :tagged) # => "[REDACTED:CONTACT]"
|
|
27
|
+
# DataRedactor.redact(text, placeholder: :hash) # => "[CONTACT_a3f9]"
|
|
28
|
+
# DataRedactor.redact(text, placeholder: :length) # => "[REDACTED:16]"
|
|
29
|
+
# DataRedactor.redact(text, placeholder: :tagged_length) # => "[REDACTED:CONTACT:16]"
|
|
28
30
|
#
|
|
29
31
|
# @example Audit / dry-run
|
|
30
32
|
# DataRedactor.scan(text)
|
|
@@ -125,12 +127,15 @@ module DataRedactor
|
|
|
125
127
|
# and/or pattern name(s).
|
|
126
128
|
# @param except [Symbol, String, Array, nil] exclude the given tag(s)
|
|
127
129
|
# and/or pattern name(s). May be combined with +only:+.
|
|
128
|
-
# @param placeholder [String, :tagged, :hash
|
|
129
|
-
# A String is used verbatim. +:tagged+ produces
|
|
130
|
-
# +:hash+ produces a deterministic +[TAGNAME_xxxx]+
|
|
131
|
-
# so the same input value always maps to the same token.
|
|
130
|
+
# @param placeholder [String, :tagged, :hash, :length, :tagged_length]
|
|
131
|
+
# replacement strategy. A String is used verbatim. +:tagged+ produces
|
|
132
|
+
# +[REDACTED:TAGNAME]+. +:hash+ produces a deterministic +[TAGNAME_xxxx]+
|
|
133
|
+
# token (4-hex djb2) so the same input value always maps to the same token.
|
|
134
|
+
# +:length+ produces +[REDACTED:N]+ and +:tagged_length+ produces
|
|
135
|
+
# +[REDACTED:TAGNAME:N]+, where +N+ is the byte length of the redacted value.
|
|
132
136
|
# @return [String] a new string with every match replaced.
|
|
133
|
-
# @raise [ArgumentError] if +placeholder:+ is not a String/:tagged/:hash
|
|
137
|
+
# @raise [ArgumentError] if +placeholder:+ is not a String/:tagged/:hash/
|
|
138
|
+
# :length/:tagged_length.
|
|
134
139
|
# @raise [UnknownTagError] if any Symbol in +only:+/+except:+ is not in {TAGS}.
|
|
135
140
|
# @raise [UnknownPatternError] if any String in +only:+/+except:+ is not in {pattern_names}.
|
|
136
141
|
#
|
|
@@ -194,7 +199,7 @@ module DataRedactor
|
|
|
194
199
|
# Any type is accepted; non-String scalars are returned as-is.
|
|
195
200
|
# @param only [Symbol, String, Array, nil] forwarded to {redact}.
|
|
196
201
|
# @param except [Symbol, String, Array, nil] forwarded to {redact}.
|
|
197
|
-
# @param placeholder [String, :tagged, :hash] forwarded to {redact}.
|
|
202
|
+
# @param placeholder [String, :tagged, :hash, :length, :tagged_length] forwarded to {redact}.
|
|
198
203
|
# @return [Hash, Array, String, Object] a new structure of the same shape
|
|
199
204
|
# with all String leaves redacted.
|
|
200
205
|
# @raise [ArgumentError] if the structure contains a circular reference.
|
|
@@ -217,7 +222,7 @@ module DataRedactor
|
|
|
217
222
|
# @param json_string [String] valid JSON input.
|
|
218
223
|
# @param only [Symbol, String, Array, nil] forwarded to {redact}.
|
|
219
224
|
# @param except [Symbol, String, Array, nil] forwarded to {redact}.
|
|
220
|
-
# @param placeholder [String, :tagged, :hash] forwarded to {redact}.
|
|
225
|
+
# @param placeholder [String, :tagged, :hash, :length, :tagged_length] forwarded to {redact}.
|
|
221
226
|
# @return [String] a JSON string with all String values redacted.
|
|
222
227
|
# @raise [JSON::ParserError] if +json_string+ is not valid JSON.
|
|
223
228
|
#
|
|
@@ -425,17 +430,20 @@ module DataRedactor
|
|
|
425
430
|
# Translate the user-facing +placeholder:+ value into the +(mode_int, str)+
|
|
426
431
|
# pair the C layer expects.
|
|
427
432
|
#
|
|
428
|
-
# @param placeholder [String, :tagged, :hash]
|
|
433
|
+
# @param placeholder [String, :tagged, :hash, :length, :tagged_length]
|
|
429
434
|
# @return [Array(Integer, String)]
|
|
430
435
|
# @raise [ArgumentError] if +placeholder+ is none of the accepted values.
|
|
431
436
|
def resolve_placeholder(placeholder)
|
|
432
437
|
case placeholder
|
|
433
|
-
when :tagged
|
|
434
|
-
when :hash
|
|
435
|
-
when
|
|
438
|
+
when :tagged then [PH_MODE_TAGGED, ""]
|
|
439
|
+
when :hash then [PH_MODE_HASH, ""]
|
|
440
|
+
when :length then [PH_MODE_LENGTH, ""]
|
|
441
|
+
when :tagged_length then [PH_MODE_TAGGED_LENGTH, ""]
|
|
442
|
+
when String then [PH_MODE_PLAIN, placeholder]
|
|
436
443
|
else
|
|
437
444
|
raise ArgumentError,
|
|
438
|
-
"placeholder must be a String, :tagged,
|
|
445
|
+
"placeholder must be a String, :tagged, :hash, :length, or :tagged_length " \
|
|
446
|
+
"— got #{placeholder.inspect}"
|
|
439
447
|
end
|
|
440
448
|
end
|
|
441
449
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: data_redactor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: x86_64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Daniele Frisanco
|
|
@@ -121,6 +121,7 @@ files:
|
|
|
121
121
|
- lib/data_redactor/integrations/rack.rb
|
|
122
122
|
- lib/data_redactor/integrations/rails.rb
|
|
123
123
|
- lib/data_redactor/name_pattern.rb
|
|
124
|
+
- lib/data_redactor/refinements.rb
|
|
124
125
|
- lib/data_redactor/version.rb
|
|
125
126
|
homepage: https://github.com/danielefrisanco/data_redactor
|
|
126
127
|
licenses:
|