Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions .dumplingconf.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ ssn = { strategy = "hash", salt = "${DUMPLING_USERS_SSN_SALT}", as_str
# password/token — hash without revealing original
password_hash = { strategy = "hash", as_string = true }
api_token = { strategy = "redact", as_string = true }
# listed under [sensitive_columns] — explicit coverage for --strict-coverage / lint-policy
employee_number = { strategy = "hash", as_string = true }
tax_id = { strategy = "hash", salt = "${DUMPLING_USERS_SSN_SALT}", as_string = true }
national_id = { strategy = "hash", salt = "${DUMPLING_USERS_SSN_SALT}", as_string = true }
# numeric fields
age = { strategy = "int_range", min = 18, max = 90 }
# random alphanumeric string of given length
Expand All @@ -52,6 +56,8 @@ ref_code = { strategy = "string", length = 12 }
external_id = { strategy = "uuid" }
# set to SQL NULL
legacy_field = { strategy = "null" }
# empty string clear (NOT NULL text); NULL source stays NULL
internal_notes = { strategy = "blank" }
# temporal fuzzing
date_of_birth = { strategy = "date_fuzz", min_days = -365, max_days = 365 }
created_at = { strategy = "datetime_fuzz", min_seconds = -86400, max_seconds = 86400 }
Expand All @@ -63,8 +69,13 @@ wake_time = { strategy = "time_fuzz", min_seconds = -3600, max_seconds = 360
credit_card = { strategy = "payment_card", length = 16, domain = "order_pan" }
# monetary / numeric — random decimal in range with fixed fractional digits
order_total = { strategy = "decimal", min = 0, max = 99999, scale = 2, domain = "order_amount" }
# keep the same anonymized email as users table via shared domain
customer_email = { strategy = "faker", faker = "internet::SafeEmail", domain = "customer_identity" }
# same anonymized email as users via shared domain (must use the same strategy)
customer_email = { strategy = "email", domain = "customer_identity" }
# listed under [sensitive_columns]
bank_account = { strategy = "string", length = 16 }
# JSON path rules — clear nested containers without wiping the whole document
"metadata.tags" = { strategy = "empty_array" }
"metadata.extras" = { strategy = "empty_object" }

[rules."public.audit_log"]
# unqualified table name also works (matches any schema)
Expand Down Expand Up @@ -109,26 +120,39 @@ delete = [
]

# Supported operators:
# eq / neq — string equality (case_insensitive = true for case-insensitive)
# in / not_in — list membership (use `values = [...]`)
# like / ilike — SQL LIKE pattern (% and _); ilike is always case-insensitive
# regex / iregex — Rust regex; iregex is case-insensitive
# lt / lte / gt / gte — numeric compare (values parsed as f64)
# is_null / not_null — NULL check (no value needed)
# eq / neq — string equality (case_insensitive = true for case-insensitive)
# in / not_in — list membership (use `values = [...]`)
# like / ilike — SQL LIKE pattern (% and _); ilike is always case-insensitive
# not_like / not_ilike — negation of like / ilike (prefer over regex look-around)
# regex / iregex — Rust regex crate (not PCRE); look-around / backrefs rejected at load
# not_regex / not_iregex — negation of regex / iregex
# lt / lte / gt / gte — numeric compare (values parsed as f64)
# is_null / not_null — NULL check (no value needed)

# ---------------------------------------------------------------------------
# [[column_cases]] — conditional per-column strategy overrides
#
# For each row and column, Dumpling evaluates cases top-to-bottom and applies
# the FIRST matching case. Falls back to [rules] if none match.
# the FIRST matching case. Falls back to [rules] if none match. If no case
# matches and there is no [rules] default, the cell is left unchanged
# (keep-by-omission).
#
# when.any = OR semantics; when.all = AND semantics; both empty = always match.
# strategy = { strategy = "keep" } is only valid under column_cases (not [rules]).
# ---------------------------------------------------------------------------
[[column_cases."public.users".email]]
# Admin emails → always redact (regardless of other conditions)
when.any = [{ column = "is_admin", op = "eq", value = "true" }]
strategy = { strategy = "redact", as_string = true }

[[column_cases."public.users".email]]
# Staff domains → keep original (default [rules] still scrubs everyone else)
when.any = [
{ column = "email", op = "ilike", value = "%@myco.com" },
{ column = "email", op = "ilike", value = "%@partner.example" },
]
strategy = { strategy = "keep" }

[[column_cases."public.users".email]]
# EU users → hash with a region-specific salt for GDPR compliance
when.any = [{ column = "country", op = "in", values = ["DE", "FR", "GB", "IT", "ES"] }]
Expand Down
16 changes: 11 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ src/
transform.rs — All anonymization strategies, PRNG, deterministic domain mapping
sql.rs — SQL stream processor: INSERT + COPY parsing, column strategy selection,
CREATE TABLE length extraction, sensitive coverage tracking
filter.rs — Row-filter predicate evaluation (eq/neq/like/regex/JSON-path/…)
filter.rs — Row-filter predicate evaluation (eq/neq/like/not_like/regex/…/JSON-path)
scan.rs — Post-transform residual PII scanner (email/SSN/PAN/token regex)
report.rs — JSON report / audit sidecar: provenance, streaming checksums, Reporter helper
compressed_input.rs — gzip/ZIP wrappers; streaming vs temp materialization
Expand Down Expand Up @@ -232,13 +232,19 @@ Follow these steps in order. Do not skip any step.

## How to Add a New Row Filter Predicate Operator

1. **`src/settings.rs` — `Predicate.op` doc comment**: Update the doc comment listing supported operator names.
1. **`src/settings.rs` — `KNOWN_PREDICATE_OPS`**: Add the operator name string to the const slice (used by validation).

2. **`src/filter.rs` — `predicate_matches`**: Add a match arm for the new operator string in the appropriate branch (`single-value` or `multi-value`).
2. **`src/settings.rs` — `Predicate.op` doc comment**: Update the doc comment listing supported operator names (and any regex / value-shape notes).

3. **Tests**: Add `#[test]` functions in `src/filter.rs`.
3. **`src/settings.rs` — `validate_predicate`**: Extend validation if the operator needs special value requirements or compile-time checks (see the `regex` / `not_regex` arms).

4. **`README.md`**: Add a row to the predicate operators table.
4. **`src/filter.rs` — `predicate_matches`**: Add a match arm for the new operator string in the appropriate branch (`single-value` or `multi-value`).

5. **`src/lint.rs`**: If the operator can be statically invalid (like regex), extend lint checks so `dumpling lint-policy` surfaces the same class of errors.

6. **Tests**: Add `#[test]` functions in `src/filter.rs` (and `settings.rs` / `lint.rs` when validation is involved).

7. **`README.md`**: Add a row to the predicate operators table; mirror the same operators in `docs/src/configuration.md` and `.dumplingconf.example` comments when the list changes.

---

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,19 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht

- **JSON `--report` audit sidecar**: Dumpling version, per-run id and timestamp, config path + SHA-256, streaming input/output SHA-256, dump-seal digest cross-link (`seal_sha256`), explicit gate flags, and coverage/scan outcomes. `run_id` / `started_at` are instance metadata; policy fingerprint fields stay deterministic across re-runs ([#13](https://github.com/ababic/dumpling/issues/13)).
- **`--no-seal`**: skip writing the dump-seal SQL comment (stdin/stdout pipelines). Incoming seals are still recognized; `--report` still records `seal_sha256`.
- **`keep` strategy** (under `[column_cases]` only): leave matching cells unchanged while a default `[rules]` entry scrubs the rest (for example staff-email allowlists). Counts toward `--strict-coverage`; kept cells are not counted as changed ([#77](https://github.com/ababic/dumpling/issues/77) / [#78](https://github.com/ababic/dumpling/pull/78)).
- **Negating predicate operators** `not_like`, `not_ilike`, `not_regex`, and `not_iregex` for row filters and `column_cases` `when` clauses ([#79](https://github.com/ababic/dumpling/issues/79) / [#81](https://github.com/ababic/dumpling/pull/81)).
- **`lint-policy` check `invalid-regex-predicate`**: flags unsupported or uncompilable `regex` / `iregex` / `not_regex` / `not_iregex` patterns ([#81](https://github.com/ababic/dumpling/pull/81)).

### Changed

- **Fail closed on invalid regex predicates**: unsupported Rust `regex` features (look-around, backreferences, possessive quantifiers) and patterns that fail to compile are rejected at config load instead of silently matching nothing ([#79](https://github.com/ababic/dumpling/issues/79) / [#81](https://github.com/ababic/dumpling/pull/81)).

### Docs

- `--help` Examples plus long help for `--report` / `--no-seal`. mdBook and README cover dump seals, the JSON sidecar fields, and how to verify `seal_sha256` with or without a seal line.
- **Design rationale** page in mdBook (strengths, alternatives, and trade-offs) ([#84](https://github.com/ababic/dumpling/pull/84)).
- Keep-by-omission semantics for cases-only columns, `keep` cookbooks, and `not_*` predicate guidance aligned across README, configuration guide, CI guardrails, and `.dumplingconf.example` ([#82](https://github.com/ababic/dumpling/pull/82), [#81](https://github.com/ababic/dumpling/pull/81)).

## [0.7.0] - 2026-07-03

Expand All @@ -24,6 +33,7 @@ Stable **0.7.0** release (supersedes **0.7.0-alpha** and **0.7.0-beta** prerelea

- **`scaffold-config` subcommand**: generate a draft starter `.dumplingconf` from a dump using heuristics and optional row sampling ([#67](https://github.com/ababic/dumpling/pull/67)).
- **`decimal` and `payment_card` anonymization strategies** with per-strategy option documentation ([#62](https://github.com/ababic/dumpling/pull/62)).
- **`blank`, `empty_array`, and `empty_object` strategies**: empty-string clear for NOT NULL text; typed JSON `[]` / `{}` clears for path rules (NULL sources stay NULL; `domain` rejected) ([#62](https://github.com/ababic/dumpling/pull/62)).
- **Gzip and ZIP inputs**: plain-SQL payloads inside **gzip** are decompressed **in-process** (streamed) when possible—no temporary file. Dumpling still materializes to the temp directory when required: **ZIP** archives (random-access central directory), **gzip wrapping `PGDMP`** or an inner **ZIP** (nested wrappers), or other cases where a filesystem path is needed for `pg_restore`. Temporary files are registered for removal when processing finishes. **`--in-place` is rejected** only when Dumpling had to write a **temporary** decompressed/extracted file (not when gzip plain-SQL streaming was used). Full multi-file ZIP packages (for example BACPAC) are still not supported as SQL input ([#70](https://github.com/ababic/dumpling/pull/70)).
- **MIT License** ([#71](https://github.com/ababic/dumpling/pull/71)).

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Follow these steps once; you will have a working path from “raw dump” to “
2. **Or start from the example policy** — Copy [`.dumplingconf.example`](.dumplingconf.example) to `.dumplingconf` (or merge under `[tool.dumpling]` in `pyproject.toml`) and edit `[rules]` by hand. Set environment variables for `salt` and any `${…}` references so Dumpling can resolve secrets at startup.
3. **Align rules with your dump** — If you did not use `scaffold-config`, open the dump beside the config: `CREATE TABLE`, `COPY … (…)`, and `INSERT INTO … (…)` lines list identifiers for `[rules."table"]` or `[rules."schema.table"]` (see [Configuration (TOML)](#configuration-toml)). Trim rules to the tables you care about first, then extend columns and strategies as you go.
4. **Run Dumpling** — `dumpling -i dump.sql -o sanitized.sql` (add `-c path` if the config is not in the default search path). Use `dumpling --check -i dump.sql` when you only want to know whether anything would change. Pass **`--no-seal`** to skip the dump-seal comment on streaming output, and **`--report file.json`** for an audit sidecar.
5. **Tighten the policy** — Run `dumpling lint-policy` on your config. When you are ready for stricter gates, add `[sensitive_columns]` and use `--strict-coverage` / `--report` / `--scan-output` as described under [Usage](#usage).
5. **Tighten the policy** — Run `dumpling lint-policy` on your config (unsalted hashes, inconsistent domains, uncovered sensitive columns, invalid regex predicates). When you are ready for stricter gates, add `[sensitive_columns]` and use `--strict-coverage` / `--report` / `--scan-output` as described under [Usage](#usage). For allowlists inside a scrubbed column, use **`keep`** under `column_cases` (see [Conditional per-column cases](#conditional-per-column-cases)).

The same flow is spelled out in the docs: [Getting started](https://ababic.github.io/dumpling/getting-started.html).

Expand Down Expand Up @@ -600,6 +600,7 @@ dumpling lint-policy --config .dumplingconf # explicit config path
| `unsalted-hash` | warning | `hash` strategy used without any salt — reversible for low-entropy inputs |
| `inconsistent-domain-strategy` | error | Same domain name used with different strategies — breaks referential integrity |
| `uncovered-sensitive-column` | error | A column in `[sensitive_columns]` has no matching rule or case |
| `invalid-regex-predicate` | error | A `regex` / `iregex` / `not_regex` / `not_iregex` pattern is missing, unsupported (look-around, backreferences, …), or fails to compile |

Exits `0` if no violations are found, `1` if any violations exist. Plug it into CI as a pre-merge gate:

Expand All @@ -624,4 +625,4 @@ See the [CI guardrails documentation](docs/src/ci-guardrails.md) for full pipeli

## Full documentation

Detailed docs, including the configuration reference and release process, are available at the project's [GitHub Pages site](https://ababic.github.io/dumpling/) (built from `docs/src/`).
Detailed docs — [Getting started](https://ababic.github.io/dumpling/getting-started.html), [Design rationale](https://ababic.github.io/dumpling/design-rationale.html), [Configuration guide](https://ababic.github.io/dumpling/configuration.html), [CI guardrails](https://ababic.github.io/dumpling/ci-guardrails.html), and the release process — are on the project's [GitHub Pages site](https://ababic.github.io/dumpling/) (built from `docs/src/`).
4 changes: 4 additions & 0 deletions docs/src/ci-guardrails.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ violations to stderr, and exits:
| `unsalted-hash` | warning | A `hash` strategy is used with no salt (neither per-column `salt` nor global `salt`). Unsalted hashes are reversible via precomputed lookup tables for low-entropy inputs (names, emails, common IDs). |
| `inconsistent-domain-strategy` | error | The same domain name is used with two or more different strategies. This breaks referential integrity: a domain shared between incompatible generators (for example `faker` with different `faker` targets, or `faker` vs `hash`) cannot maintain a single stable mapping. |
| `uncovered-sensitive-column` | error | A column listed in `[sensitive_columns]` has no matching anonymization rule or case. The column will pass through unmodified, making the sensitive declaration misleading. |
| `invalid-regex-predicate` | error | A `regex` / `iregex` / `not_regex` / `not_iregex` pattern in `row_filters` or `column_cases` is missing, uses unsupported Rust `regex` features (look-around, backreferences, possessive quantifiers), or fails to compile. Prefer `not_like` / `not_ilike` / `not_regex` / `not_iregex`, or a default scrub plus `keep`, instead of negative lookahead. |

---

Expand Down Expand Up @@ -204,3 +205,6 @@ Archive `report.json` with the sanitized SQL. To confirm a later re-run used the
- In hardened security profile environments, a global `salt` is required anyway
(`--security-profile hardened` will error without it), so `unsalted-hash`
warnings become informational.
- Prefer `keep` under `column_cases` (or `not_ilike` / `not_iregex`) for
allowlist-style exceptions; do not rely on regex look-around — it is rejected
at config load and by `invalid-regex-predicate`.
Loading
Loading