Threat model¶
What FreshData defends against, what it deliberately does not, and where the residual risks live. Limitations is capability honesty; this page is security and privacy honesty. Every claim here is backed by a test, a measurement, or a pointer to the line of code that enforces it.
Trust boundaries¶
1. Input data → cleaning engine¶
Input is untrusted. Cleaning never executes cell contents: there is no
eval, no exec, and no pickle loading anywhere in src/freshdata,
including the format parsers (HL7v2 / GPX / SDMX / EDIFACT / FHIR), which
are hand-rolled text parsers. Inputs are read only through
pandas / pyarrow / DuckDB readers.
2. Cleaned data → CSV exports (formula injection)¶
Cells starting with = + - @ <tab> <cr> execute as formulas when a CSV is
opened in Excel / Google Sheets / LibreOffice (OWASP CSV injection).
FreshData's stance follows the artifact's purpose:
| Surface | Default | Why |
|---|---|---|
export_review_queue (csv) |
sanitize on (sanitize_formulas=False to opt out) |
built to be opened by humans in spreadsheets |
fd.clean_csv(output_path=...) |
off, opt-in sanitize_formulas=True |
pipeline artifact; byte-exact fidelity by default |
| streaming CLI | off, opt-in --sanitize-formulas (also covers the quarantine export) |
same |
JSONL and Parquet are never altered. Residual risk: a pipeline CSV opened directly in a spreadsheet keeps the risk unless the flag was passed.
3. DataFrame → AI Copilot model_context¶
report.model_context is the only payload a provider hook ever sees, and
it is SHA-256 fingerprinted in report.audit so you can prove after the
fact what was shared. Per privacy mode:
mask_pii_before_reasoning(default) — every string-like column (object / string / categorical) in the sample rows is hash-masked: declaredmust_maskcolumns, regex-detected PII columns, and everything else string-like. This is deliberate defense-in-depth: regex detection cannot see names, addresses, or free text, so no string value is trusted to be safe.allow_unmasked_columnsis an explicit per-column opt-out that never exempts a declared or detected PII column and rejects unknown names. Detected-problem details enteringmodel_contextare value-free in every mode (category_noisespelling previews stay local).schema_only— no cell values at all.
These guarantees are enforced by adversarial regression tests registered in
the CLAIM_REGISTRY (tests/test_experimental_ai_copilot.py), which CI
re-verifies against the README wording.
Residual risk (by design, documented): numeric values pass through
unmasked. Numeric quasi-identifiers — an exact salary plus age plus a
postcode-like code — can re-identify a person. Drop such columns first or
use schema_only.
4. Provider hook failure¶
Provider exceptions are caught, recorded as report.audit["provider_error"],
and the deterministic report survives. Stated plainly: the prompt has
already been sent when a provider fails — a failing provider does not
un-send data. The failure mode is fail-closed for the report, not a
retroactive privacy guarantee.
5. PII detection is regex-only¶
The dependency-free detector covers EMAIL / PHONE / SSN / credit card / IP.
It does not detect names, addresses, or free-text PII (install the
privacy extra for NER via presidio). This is the #1 practitioner gotcha:
if you call anonymize() with only auto-detected columns, undetected PII
passes through. The copilot does not inherit this gap because it masks all
string-like columns regardless of detection (see boundary 3).
6. Masking tokens¶
Hash masking is HMAC-SHA256 with a configurable salt. The default salt is
a public constant in the source, so tokens are stable across runs and
joinable — and an attacker holding tokens can confirm guesses of
low-cardinality values. Supply your own salt on MaskingRule when
unlinkability matters. The copilot's internal masking uses the default
deterministic path on purpose: a per-run random salt would break the
documented reproducibility of model_context and its audit fingerprint.
This trade-off is tracked as a roadmap item, not silently changed.
Non-goals¶
- Not a sandbox. FreshData reads tabular files; hostile file formats are the parser libraries' concern (pandas / pyarrow / duckdb versions are therefore floor-pinned).
- Not DLP. PII detection is best-effort convenience. The compliance packs state their own scope and do not turn detection into a guarantee.
- No network in any cleaning path. The only network call in the package
is the explicit
fd.models.pull(...); offline behavior is CI-enforced (-m "not online"is the default gate).