data_redactor 0.8.0 → 0.10.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.
data/readme.md CHANGED
@@ -8,7 +8,33 @@ A Ruby gem with a C extension for high-performance regex-based redaction of sens
8
8
 
9
9
  ## What it does
10
10
 
11
- DataRedactor scans text for sensitive patterns and replaces matches with `[REDACTED]`. It uses a C extension backed by POSIX `regex.h` so the heavy lifting happens outside the Ruby VM, making it fast enough for large payloads.
11
+ DataRedactor scans text for sensitive data API keys and cloud secrets, IBANs,
12
+ credit cards, national IDs, emails, phone numbers, IPs, and more — and replaces
13
+ each match with a placeholder. The scanning runs in a C extension backed by a
14
+ zero-dependency Thompson NFA → lazy-DFA multi-pattern engine (v19) that scans
15
+ all 88 built-in patterns in a single pass — 2–2.5× faster than pure-Ruby `gsub`
16
+ on large payloads, with no external library dependencies.
17
+
18
+ It ships **88 built-in patterns** across 15+ countries, grouped into tags
19
+ (`:credentials`, `:financial`, `:contact`, ...) so you can redact only what you
20
+ care about. Beyond plain strings it can walk nested Hashes, Arrays, and JSON,
21
+ audit a payload without mutating it (`scan`), and plug into Logger, Rails, and
22
+ Rack. You can also register your own patterns at boot.
23
+
24
+ ### Use cases
25
+
26
+ - **Log scrubbing** — drop the `Logger` formatter in so no secret or PII ever
27
+ reaches disk or your log aggregator.
28
+ - **Rails parameter filtering** — feed `filter_parameters` a redactor-backed proc
29
+ to keep request params out of logs and error reports.
30
+ - **HTTP request/response sanitising** — Rack middleware scrubs response bodies
31
+ and sensitive headers in flight.
32
+ - **Sanitising LLM / API payloads** — run `redact_deep` over a params hash or
33
+ `redact_json` over a JSON body before it leaves the process.
34
+ - **Compliance & auditing** — `scan` reports every match with byte offsets, tag,
35
+ and pattern name without changing the text, for false-positive tuning.
36
+ - **Internal identifiers** — register company-specific patterns (`add_pattern`)
37
+ or generate them from a person's name (`name_pattern`).
12
38
 
13
39
  ## Usage
14
40
 
@@ -158,6 +184,46 @@ DataRedactor.clear_custom_patterns! # mostly for test suites
158
184
 
159
185
  **`boundary: true`** — wraps the pattern with `(^|[^0-9A-Za-z])(PATTERN)([^0-9A-Za-z]|$)` so it only fires when the token is not embedded in a longer alphanumeric string. Incompatible with patterns that contain capture groups.
160
186
 
187
+ ### Name patterns
188
+
189
+ Personal names can't ship as built-ins — every team has different ones — but the regex
190
+ boilerplate to match a name across its written variations is the same every time.
191
+ `name_pattern` generates that regex for you, ready to hand to `add_pattern`:
192
+
193
+ ```ruby
194
+ DataRedactor.add_pattern(
195
+ name: "person_mario_rossi",
196
+ regex: DataRedactor.name_pattern("Mario", "Rossi"),
197
+ tag: :contact
198
+ )
199
+
200
+ DataRedactor.redact("ticket from Mario Rossi about ...")
201
+ # => "ticket from [REDACTED] about ..."
202
+ ```
203
+
204
+ A single generated pattern matches all of these:
205
+
206
+ - **Case** — `Mario Rossi`, `mario rossi`, `MARIO ROSSI`
207
+ - **Order** — `Mario Rossi`, `Rossi Mario`, `Rossi, Mario`, `Rossi,Mario`
208
+ - **Initials** — `M. Rossi`, `M Rossi`, `Mario R.`, `M.R.`, `MR`
209
+ - **Diacritics** — `name_pattern("Jose", "Munoz")` also matches `José Muñoz` (and vice versa)
210
+ - **Separators** — spaces and hyphens are interchangeable. `name_pattern("Anne-Marie", "Berg")`
211
+ matches `Anne-Marie Berg`, `Anne Marie Berg`, `AnneMarie Berg`, and each half alone
212
+ (`Anne Berg`, `Marie Berg`). Multi-word parts like `"Van der Berg"` tolerate any
213
+ space/hyphen separator between words.
214
+
215
+ It does **not** match a name embedded in a longer word — `Mario` will not fire inside
216
+ `Mariolino` — because the generated pattern is boundary-wrapped. For that reason, register
217
+ it with the default `boundary: false` (the wrapper is already baked into the returned
218
+ string; `boundary: true` would double-wrap and reject its capture groups).
219
+
220
+ Pass `middle:` to also cover a middle name — both the no-middle and with-middle forms match:
221
+
222
+ ```ruby
223
+ DataRedactor.name_pattern("Mario", "Rossi", middle: "Luigi")
224
+ # matches "Mario Rossi" AND "Mario Luigi Rossi" AND "Rossi Mario Luigi"
225
+ ```
226
+
161
227
  ## Integrations
162
228
 
163
229
  Optional adapters for Logger, Rails, and Rack. None are loaded automatically — `require` only what you use, and the gem adds zero runtime dependencies in the gemspec.
@@ -306,7 +372,9 @@ redactor/
306
372
  ├── lib/
307
373
  │ ├── data_redactor.rb # Ruby entry point, loads the .so
308
374
  │ └── data_redactor/
309
- └── version.rb
375
+ ├── version.rb
376
+ │ ├── name_pattern.rb # name_pattern helper — generates a name regex for add_pattern
377
+ │ └── integrations/ # soft-required Logger / Rails / Rack adapters
310
378
  ├── ext/
311
379
  │ └── data_redactor/
312
380
  │ ├── extconf.rb # Checks for C headers, generates Makefile (globs *.c)
@@ -317,8 +385,18 @@ redactor/
317
385
  │ ├── scan.{c,h} # _scan + byte-offset replacement-log macros
318
386
  │ ├── custom_patterns.{c,h} # Dynamic registry: add/remove/clear/list
319
387
  │ └── tags.h # TAG_* bit constants
320
- └── spec/
321
- └── data_redactor_spec.rb # RSpec tests — at least one example per pattern, plus filter / placeholder / custom-pattern coverage
388
+ ├── spec/
389
+ └── data_redactor_spec.rb # RSpec tests — at least one example per pattern, plus filter / placeholder / custom-pattern coverage
390
+ ├── benchmark/ # Repo-only perf scripts (not packaged in the gem)
391
+ │ ├── README.md # How to run, what each script measures
392
+ │ ├── support/corpus.rb # Shared payload builders + pure-Ruby baseline redactor
393
+ │ ├── throughput.rb # MB/s on representative payloads
394
+ │ ├── vs_pure_ruby.rb # C extension vs pure-Ruby gsub (same 88 patterns)
395
+ │ ├── scaling.rb # Runtime vs input size 1KB → 50MB
396
+ │ └── per_pattern.rb # Per-pattern scan cost
397
+ └── docs/ # Design and execution docs for future work
398
+ ├── standalone_matcher_design.md
399
+ └── combined_matcher_plan.md
322
400
  ```
323
401
 
324
402
  ## Requirements
@@ -393,6 +471,45 @@ Or compile and test in one step:
393
471
  bundle exec rake
394
472
  ```
395
473
 
474
+ ## Benchmarks
475
+
476
+ The `benchmark/` directory holds four scripts that measure the C engine under
477
+ different angles. They are **not** packaged with the gem.
478
+
479
+ ```bash
480
+ bundle install # pulls benchmark-ips, benchmark-memory (dev deps)
481
+ bundle exec rake compile
482
+ bundle exec ruby benchmark/vs_pure_ruby.rb # head-to-head vs pure-Ruby gsub, same 88 patterns
483
+ bundle exec ruby benchmark/throughput.rb # MB/s on a log line, JSON, 1MB and 10MB log files
484
+ bundle exec ruby benchmark/scaling.rb # runtime vs input size (1KB → 50MB), confirms linear scaling
485
+ bundle exec ruby benchmark/per_pattern.rb # per-pattern scan cost over a 1MB payload
486
+ ```
487
+
488
+ See [`benchmark/README.md`](benchmark/README.md) for what each script measures
489
+ and how the pure-Ruby baseline is kept honest (it reads the same patterns the
490
+ C engine uses, via `DataRedactor::BUILTIN_PATTERN_SOURCES`).
491
+
492
+ ### Performance (0.10.0 — v19 multi-pattern engine)
493
+
494
+ As of 0.10.0 the C extension runs a **Thompson NFA → lazy-DFA multi-pattern
495
+ engine** (v19) that scans the input once across all 88 built-in patterns,
496
+ with two selective-merge passes (pure-digit group + IBAN union) that further
497
+ reduce work for the most common pattern classes. Custom patterns (`add_pattern`)
498
+ still use the glibc path (required for correct UTF-8 diacritic matching).
499
+
500
+ | Payload | v19 engine (0.10.0) | Pure-Ruby `gsub` | Ratio |
501
+ |-----------------------|---------------------|------------------|-----------------|
502
+ | log line (168 B) | 41 µs / call | 71 µs / call | **1.7× faster** |
503
+ | JSON blob (~580 B) | 81 µs / call | 132 µs / call | **1.6× faster** |
504
+ | 8 log lines (1.3 KB) | 175 µs / call | 399 µs / call | **2.3× faster** |
505
+ | 100 log lines (17 KB) | 2.0 ms / call | 4.6 ms / call | **2.3× faster** |
506
+ | 1 MB log | 138 ms / call | 294 ms / call | **2.1× faster** |
507
+ | 10 MB log | 1.44 s / call | — | 6.9 MB/s |
508
+
509
+ All payload sizes pass a correctness check (redaction count matches pure-Ruby `gsub`).
510
+ The previous engine (per-pattern `regexec`) was **4.25× slower** than pure Ruby on the
511
+ 1 MB payload — a ~9× swing. Old numbers are in git history (`CHANGELOG.md` [0.9.0]).
512
+
396
513
  ## How it works
397
514
 
398
515
  1. At load time, `Init_data_redactor` compiles all 85 regex patterns once using `regcomp` (POSIX ERE) and stores them as static `regex_t` structs. Patterns marked as boundary-wrapped are expanded with `wrap_boundary()` before compilation.
@@ -423,3 +540,4 @@ Released under the [MIT License](LICENSE).
423
540
  - **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.
424
541
  - **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.
425
542
  - **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.
543
+ - **Performance is currently slower than pure-Ruby `gsub`.** A May 2026 investigation found the C extension is 3–5× slower than a pure-Ruby `gsub` loop running the same 88 patterns, across input sizes from 168 bytes to 1 MB. The root cause is glibc's POSIX `regexec()`: each call allocates an O(input-length) state buffer before any matching begins, and the gem calls it once per pattern in sequence. Ruby's Onigmo engine wins by using a built-in Boyer-Moore literal pre-filter that this gem can only approximate. Two perf fixes have shipped (buffer-sizing in `replace_all_matches`, a `strstr` literal pre-filter, and input chunking for large payloads), which gave ~25-30% improvement and made scaling linear, but the absolute gap remains. Use the gem on small payloads where the absolute latency is still acceptable (< 1 ms for typical log lines); for high-throughput pipelines, hold off until the next major release. See `docs/standalone_matcher_design.md` for the long-term plan.
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.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniele Frisanco
@@ -79,6 +79,34 @@ dependencies:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '2.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: benchmark-ips
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.13'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.13'
96
+ - !ruby/object:Gem::Dependency
97
+ name: benchmark-memory
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.2'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.2'
82
110
  description: A Ruby gem with a C extension for high-performance scanning and redaction
83
111
  of 85 sensitive patterns — API keys, tokens, credentials, IBANs, national IDs, emails,
84
112
  phone numbers, and PII from 15+ countries. Optional Logger formatter, Rails filter_parameters
@@ -97,6 +125,8 @@ files:
97
125
  - ext/data_redactor/custom_patterns.h
98
126
  - ext/data_redactor/data_redactor.c
99
127
  - ext/data_redactor/extconf.rb
128
+ - ext/data_redactor/matcher.c
129
+ - ext/data_redactor/matcher.h
100
130
  - ext/data_redactor/patterns.c
101
131
  - ext/data_redactor/patterns.h
102
132
  - ext/data_redactor/placeholder.c
@@ -110,6 +140,7 @@ files:
110
140
  - lib/data_redactor/integrations/logger.rb
111
141
  - lib/data_redactor/integrations/rack.rb
112
142
  - lib/data_redactor/integrations/rails.rb
143
+ - lib/data_redactor/name_pattern.rb
113
144
  - lib/data_redactor/version.rb
114
145
  - readme.md
115
146
  homepage: https://github.com/danielefrisanco/data_redactor