maskflow-cli 0.3.0__tar.gz → 0.6.0__tar.gz
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.
- maskflow_cli-0.6.0/.dockerignore +3 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/.gitignore +1 -0
- maskflow_cli-0.6.0/Dockerfile +36 -0
- maskflow_cli-0.6.0/PKG-INFO +107 -0
- maskflow_cli-0.6.0/README.md +86 -0
- maskflow_cli-0.6.0/examples/README.md +83 -0
- maskflow_cli-0.6.0/examples/generate_sample.py +119 -0
- maskflow_cli-0.6.0/examples/sample-llm-traffic.jsonl +60 -0
- maskflow_cli-0.6.0/packaging/README.md +56 -0
- maskflow_cli-0.6.0/packaging/_entry.py +23 -0
- maskflow_cli-0.6.0/packaging/maskflow.spec +75 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/pyproject.toml +31 -7
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/app.py +2 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/__init__.py +25 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/aggregate.py +178 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/checkpoint.py +81 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/cmd.py +287 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/errors.py +20 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/fieldsel.py +95 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/pipeline.py +265 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/report/__init__.py +25 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/report/assets.py +151 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/report/build.py +178 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/report/csv_out.py +42 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/report/html.py +290 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/report/json_out.py +22 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/report/summary.py +78 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/severity.py +182 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/__init__.py +61 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/_api_common.py +40 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/_files.py +112 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/_http.py +72 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/_meta.py +44 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/base.py +105 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/csv.py +68 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/dir.py +136 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/helicone.py +100 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/jsonl.py +87 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/langfuse.py +108 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/langsmith.py +112 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/postgres.py +124 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/sources/s3.py +143 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/spec.py +71 -0
- maskflow_cli-0.6.0/src/maskflow_cli/scan/worker.py +183 -0
- maskflow_cli-0.6.0/tests/scan/__init__.py +0 -0
- maskflow_cli-0.6.0/tests/scan/_fuzz_corpus.py +61 -0
- maskflow_cli-0.6.0/tests/scan/conftest.py +12 -0
- maskflow_cli-0.6.0/tests/scan/test_aggregate_and_severity.py +93 -0
- maskflow_cli-0.6.0/tests/scan/test_example_file.py +87 -0
- maskflow_cli-0.6.0/tests/scan/test_fieldsel.py +50 -0
- maskflow_cli-0.6.0/tests/scan/test_pipeline_and_report.py +189 -0
- maskflow_cli-0.6.0/tests/scan/test_report_no_pii_leak.py +97 -0
- maskflow_cli-0.6.0/tests/scan/test_sources_api.py +62 -0
- maskflow_cli-0.6.0/tests/scan/test_sources_files.py +107 -0
- maskflow_cli-0.3.0/PKG-INFO +0 -42
- maskflow_cli-0.3.0/README.md +0 -26
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/__init__.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/commands/__init__.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/commands/config_cmd.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/commands/doctor_cmd.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/commands/explain_cmd.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/doctor.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/doctor_render.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/explain.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/explain_render.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/src/maskflow_cli/render.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/conftest.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/fixtures/partial.toml +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/fixtures/typo.toml +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/fixtures/valid.toml +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/test_cli_doctor.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/test_cli_explain.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/test_cli_show.py +0 -0
- {maskflow_cli-0.3.0 → maskflow_cli-0.6.0}/tests/test_cli_validate.py +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# `maskflow` CLI -- full image, including the spaCy NER pass so
|
|
2
|
+
# `maskflow scan --deep` (bare names & addresses) works out of the box.
|
|
3
|
+
#
|
|
4
|
+
# Build (nothing is COPY'd, so the context is just this directory):
|
|
5
|
+
# docker build -f packages/maskflow-cli/Dockerfile \
|
|
6
|
+
# --build-arg MASKFLOW_CLI_VERSION=0.6.0 \
|
|
7
|
+
# -t maskflow/cli packages/maskflow-cli
|
|
8
|
+
#
|
|
9
|
+
# Run -- mount the directory holding your data as /work:
|
|
10
|
+
# docker run --rm -v "$PWD:/work" ghcr.io/maskflow/cli \
|
|
11
|
+
# scan jsonl /work/requests.jsonl --field 'messages[].content' \
|
|
12
|
+
# --deep -o /work/exposure-report.html
|
|
13
|
+
#
|
|
14
|
+
# Published as ghcr.io/maskflow/cli:<version> and :latest by
|
|
15
|
+
# .github/workflows/release-cli.yml on a `cli-py-v*` tag.
|
|
16
|
+
|
|
17
|
+
FROM python:3.12-slim
|
|
18
|
+
|
|
19
|
+
# Empty => install the latest published maskflow-cli. A release build pins
|
|
20
|
+
# the exact version so the image matches the tag.
|
|
21
|
+
ARG MASKFLOW_CLI_VERSION=""
|
|
22
|
+
|
|
23
|
+
RUN set -eux; \
|
|
24
|
+
pip install --no-cache-dir \
|
|
25
|
+
"maskflow-cli[s3,postgres]${MASKFLOW_CLI_VERSION:+==${MASKFLOW_CLI_VERSION}}"; \
|
|
26
|
+
python -m spacy download en_core_web_sm; \
|
|
27
|
+
python -c "import maskflow_cli.app, maskflow_pack_india, maskflow_pack_intl"; \
|
|
28
|
+
maskflow --help >/dev/null
|
|
29
|
+
|
|
30
|
+
# Unprivileged; /work is the mount point for the caller's data.
|
|
31
|
+
RUN useradd --create-home --uid 10001 scanner
|
|
32
|
+
USER scanner
|
|
33
|
+
WORKDIR /work
|
|
34
|
+
|
|
35
|
+
ENTRYPOINT ["maskflow"]
|
|
36
|
+
CMD ["--help"]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: maskflow-cli
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Command-line interface for MaskFlow: .maskflowrc config validation and inspection
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: httpx>=0.27
|
|
8
|
+
Requires-Dist: maskflow-core[yaml]<0.7,>=0.6.0
|
|
9
|
+
Requires-Dist: maskflow-pack-india<0.6,>=0.1.0
|
|
10
|
+
Requires-Dist: maskflow-pack-intl<0.4,>=0.3.0
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Requires-Dist: tomli-w>=1.0
|
|
13
|
+
Requires-Dist: typer>=0.12
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
16
|
+
Provides-Extra: postgres
|
|
17
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
|
|
18
|
+
Provides-Extra: s3
|
|
19
|
+
Requires-Dist: boto3>=1.34; extra == 's3'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# maskflow-cli
|
|
23
|
+
|
|
24
|
+
Command-line interface for [MaskFlow](https://github.com/):
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
maskflow config validate
|
|
28
|
+
maskflow config show --resolved
|
|
29
|
+
maskflow doctor
|
|
30
|
+
maskflow explain "<text>"
|
|
31
|
+
maskflow scan jsonl requests.jsonl --field 'messages[].content'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`maskflow doctor` checks installed versions, spaCy model presence (and
|
|
35
|
+
which entities that consequently disables), and `.maskflowrc` validity,
|
|
36
|
+
then reports enabled/disabled status for every registered entity. It
|
|
37
|
+
exits 0 only when every check passes.
|
|
38
|
+
|
|
39
|
+
`maskflow explain "<text>"` shows, span by span, why each piece of text
|
|
40
|
+
was (or wasn't) detected as PII -- the pattern/NER hit, checksum result,
|
|
41
|
+
context boost, and the threshold decision behind it. Spans that scored
|
|
42
|
+
below their entity's threshold are listed separately as NEAREST MISSES,
|
|
43
|
+
with the `.maskflowrc` change that would catch them. Matched text is
|
|
44
|
+
truncated to 8 characters unless `--full` is passed. Accepts the same
|
|
45
|
+
`--config`/`--set` overrides as `maskflow config`, so explanations reflect
|
|
46
|
+
the same resolved config a real `mask()` call would use.
|
|
47
|
+
|
|
48
|
+
## `maskflow scan` -- what PII already reached your LLM providers
|
|
49
|
+
|
|
50
|
+
`maskflow scan SOURCE ...` answers the question a DPDP-deadline audit asks
|
|
51
|
+
first: **what PII has this system already sent to third-party LLM
|
|
52
|
+
providers, and how bad is it?** It reads your historical LLM traffic, runs
|
|
53
|
+
MaskFlow's own detection over it, and writes **one self-contained HTML
|
|
54
|
+
report** -- inline CSS/JS, zero external requests, so it prints cleanly and
|
|
55
|
+
can be emailed to an auditor as-is.
|
|
56
|
+
|
|
57
|
+
**Features**
|
|
58
|
+
|
|
59
|
+
- **Eight source adapters**, one interface: `jsonl` / `ndjson` (with
|
|
60
|
+
`--field` selectors), `csv` (`--columns`), `dir` (recursive), `s3`
|
|
61
|
+
(streamed), `postgres` (server-side cursor), and the `langfuse` /
|
|
62
|
+
`helicone` / `langsmith` REST APIs. `s3` and `postgres` need the
|
|
63
|
+
`maskflow-cli[s3]` / `[postgres]` extras; the rest need nothing extra.
|
|
64
|
+
- **Streaming, bounded memory** -- inputs can be gigabytes. `--workers N`
|
|
65
|
+
parallelises detection; `--checkpoint FILE` makes a run resumable;
|
|
66
|
+
`--sample N` is a fast first pass.
|
|
67
|
+
- **Hybrid detection.** The pattern/checksum pass (Aadhaar, PAN, GSTIN,
|
|
68
|
+
UPI, IFSC, cards, email, ...) covers the whole corpus. The NER pass
|
|
69
|
+
(bare names & addresses) runs on a sample and is reported as a clearly
|
|
70
|
+
labelled estimate -- pass `--deep` to run it over everything.
|
|
71
|
+
- **The report**: one headline number, breakdowns by entity type /
|
|
72
|
+
provider / model / time, a severity ranking with a plain-English "why
|
|
73
|
+
this matters" per row, **masked excerpts only** (values shown as
|
|
74
|
+
`<AADHAAR_1>`, never raw), and a DPDP Rule 6 mapping appendix. Also
|
|
75
|
+
`--format json|csv`.
|
|
76
|
+
- **Runs entirely locally. Nothing is transmitted.** The API sources only
|
|
77
|
+
*read* from your own observability account.
|
|
78
|
+
|
|
79
|
+
**Try it** -- a synthetic 60-record sample ships in
|
|
80
|
+
[`examples/`](examples/):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv run maskflow scan jsonl packages/maskflow-cli/examples/sample-llm-traffic.jsonl \
|
|
84
|
+
--field 'messages[].content' \
|
|
85
|
+
--provider-field provider --service-field model --timestamp-field created_at \
|
|
86
|
+
--deep --out exposure-report.html
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Notes: `uv run` runs the CLI from the workspace venv -- drop it if
|
|
90
|
+
`maskflow-cli` is on your `PATH` (`pipx install maskflow-cli`). Quote the
|
|
91
|
+
`--field` value: `messages[].content` contains `[]`, which the shell would
|
|
92
|
+
otherwise try to expand.
|
|
93
|
+
|
|
94
|
+
**Install it** -- four ways, see
|
|
95
|
+
[`packaging/`](packaging/) and [`docs/scan.md`](../../docs/scan.md#install):
|
|
96
|
+
|
|
97
|
+
| | NER (names/addresses/DOB) |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `pipx install maskflow-cli` + `python -m spacy download en_core_web_sm` | yes |
|
|
100
|
+
| `docker run --rm -v "$PWD:/work" ghcr.io/maskflow/cli scan ...` | yes, baked in |
|
|
101
|
+
| standalone binary (GitHub Releases, no Python) | no -- pattern pass only |
|
|
102
|
+
| [`maskflow/scan-action`](../../packaging/scan-action/) for CI | yes |
|
|
103
|
+
|
|
104
|
+
Then open `exposure-report.html`. See [`examples/README.md`](examples/README.md)
|
|
105
|
+
for a walk-through of the output, and `docs/scan.md` for the full reference.
|
|
106
|
+
|
|
107
|
+
See `docs/configuration.md` in the repo root for the full config reference.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# maskflow-cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for [MaskFlow](https://github.com/):
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
maskflow config validate
|
|
7
|
+
maskflow config show --resolved
|
|
8
|
+
maskflow doctor
|
|
9
|
+
maskflow explain "<text>"
|
|
10
|
+
maskflow scan jsonl requests.jsonl --field 'messages[].content'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`maskflow doctor` checks installed versions, spaCy model presence (and
|
|
14
|
+
which entities that consequently disables), and `.maskflowrc` validity,
|
|
15
|
+
then reports enabled/disabled status for every registered entity. It
|
|
16
|
+
exits 0 only when every check passes.
|
|
17
|
+
|
|
18
|
+
`maskflow explain "<text>"` shows, span by span, why each piece of text
|
|
19
|
+
was (or wasn't) detected as PII -- the pattern/NER hit, checksum result,
|
|
20
|
+
context boost, and the threshold decision behind it. Spans that scored
|
|
21
|
+
below their entity's threshold are listed separately as NEAREST MISSES,
|
|
22
|
+
with the `.maskflowrc` change that would catch them. Matched text is
|
|
23
|
+
truncated to 8 characters unless `--full` is passed. Accepts the same
|
|
24
|
+
`--config`/`--set` overrides as `maskflow config`, so explanations reflect
|
|
25
|
+
the same resolved config a real `mask()` call would use.
|
|
26
|
+
|
|
27
|
+
## `maskflow scan` -- what PII already reached your LLM providers
|
|
28
|
+
|
|
29
|
+
`maskflow scan SOURCE ...` answers the question a DPDP-deadline audit asks
|
|
30
|
+
first: **what PII has this system already sent to third-party LLM
|
|
31
|
+
providers, and how bad is it?** It reads your historical LLM traffic, runs
|
|
32
|
+
MaskFlow's own detection over it, and writes **one self-contained HTML
|
|
33
|
+
report** -- inline CSS/JS, zero external requests, so it prints cleanly and
|
|
34
|
+
can be emailed to an auditor as-is.
|
|
35
|
+
|
|
36
|
+
**Features**
|
|
37
|
+
|
|
38
|
+
- **Eight source adapters**, one interface: `jsonl` / `ndjson` (with
|
|
39
|
+
`--field` selectors), `csv` (`--columns`), `dir` (recursive), `s3`
|
|
40
|
+
(streamed), `postgres` (server-side cursor), and the `langfuse` /
|
|
41
|
+
`helicone` / `langsmith` REST APIs. `s3` and `postgres` need the
|
|
42
|
+
`maskflow-cli[s3]` / `[postgres]` extras; the rest need nothing extra.
|
|
43
|
+
- **Streaming, bounded memory** -- inputs can be gigabytes. `--workers N`
|
|
44
|
+
parallelises detection; `--checkpoint FILE` makes a run resumable;
|
|
45
|
+
`--sample N` is a fast first pass.
|
|
46
|
+
- **Hybrid detection.** The pattern/checksum pass (Aadhaar, PAN, GSTIN,
|
|
47
|
+
UPI, IFSC, cards, email, ...) covers the whole corpus. The NER pass
|
|
48
|
+
(bare names & addresses) runs on a sample and is reported as a clearly
|
|
49
|
+
labelled estimate -- pass `--deep` to run it over everything.
|
|
50
|
+
- **The report**: one headline number, breakdowns by entity type /
|
|
51
|
+
provider / model / time, a severity ranking with a plain-English "why
|
|
52
|
+
this matters" per row, **masked excerpts only** (values shown as
|
|
53
|
+
`<AADHAAR_1>`, never raw), and a DPDP Rule 6 mapping appendix. Also
|
|
54
|
+
`--format json|csv`.
|
|
55
|
+
- **Runs entirely locally. Nothing is transmitted.** The API sources only
|
|
56
|
+
*read* from your own observability account.
|
|
57
|
+
|
|
58
|
+
**Try it** -- a synthetic 60-record sample ships in
|
|
59
|
+
[`examples/`](examples/):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv run maskflow scan jsonl packages/maskflow-cli/examples/sample-llm-traffic.jsonl \
|
|
63
|
+
--field 'messages[].content' \
|
|
64
|
+
--provider-field provider --service-field model --timestamp-field created_at \
|
|
65
|
+
--deep --out exposure-report.html
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Notes: `uv run` runs the CLI from the workspace venv -- drop it if
|
|
69
|
+
`maskflow-cli` is on your `PATH` (`pipx install maskflow-cli`). Quote the
|
|
70
|
+
`--field` value: `messages[].content` contains `[]`, which the shell would
|
|
71
|
+
otherwise try to expand.
|
|
72
|
+
|
|
73
|
+
**Install it** -- four ways, see
|
|
74
|
+
[`packaging/`](packaging/) and [`docs/scan.md`](../../docs/scan.md#install):
|
|
75
|
+
|
|
76
|
+
| | NER (names/addresses/DOB) |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `pipx install maskflow-cli` + `python -m spacy download en_core_web_sm` | yes |
|
|
79
|
+
| `docker run --rm -v "$PWD:/work" ghcr.io/maskflow/cli scan ...` | yes, baked in |
|
|
80
|
+
| standalone binary (GitHub Releases, no Python) | no -- pattern pass only |
|
|
81
|
+
| [`maskflow/scan-action`](../../packaging/scan-action/) for CI | yes |
|
|
82
|
+
|
|
83
|
+
Then open `exposure-report.html`. See [`examples/README.md`](examples/README.md)
|
|
84
|
+
for a walk-through of the output, and `docs/scan.md` for the full reference.
|
|
85
|
+
|
|
86
|
+
See `docs/configuration.md` in the repo root for the full config reference.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# `maskflow scan` example
|
|
2
|
+
|
|
3
|
+
`sample-llm-traffic.jsonl` is **60 fully synthetic** LLM API request records
|
|
4
|
+
— the kind of log a team might export from an OpenAI/Anthropic gateway or an
|
|
5
|
+
observability tool. About three quarters of them contain planted PII
|
|
6
|
+
(Aadhaar, PAN, GSTIN, UPI VPA, IFSC, Indian mobile, passport, driving
|
|
7
|
+
licence, credit card, date of birth, names, postal addresses); the rest are
|
|
8
|
+
clean support queries.
|
|
9
|
+
|
|
10
|
+
> **Nothing here is real.** Every identifier is generated to be
|
|
11
|
+
> checksum-/format-valid but drawn at random within that shape — never from
|
|
12
|
+
> or against a real registry. Names, emails (`@*.example`), and addresses are
|
|
13
|
+
> assembled from synthetic pools. See `generate_sample.py`.
|
|
14
|
+
|
|
15
|
+
Each record looks like:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{"request_id": "req_0007", "provider": "anthropic", "model": "claude-sonnet-4",
|
|
19
|
+
"created_at": "2026-03-20T14:05:00Z",
|
|
20
|
+
"messages": [{"role": "user", "content": "Run a KYC check for ... Aadhaar ... PAN ..."}]}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Run it
|
|
24
|
+
|
|
25
|
+
From the repo root. `uv run` invokes the CLI from the workspace venv without
|
|
26
|
+
a separate install; if you have `maskflow-cli` installed (`pipx install
|
|
27
|
+
maskflow-cli`, or an activated venv), drop the `uv run` prefix.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv run maskflow scan jsonl packages/maskflow-cli/examples/sample-llm-traffic.jsonl \
|
|
31
|
+
--field 'messages[].content' \
|
|
32
|
+
--provider-field provider \
|
|
33
|
+
--service-field model \
|
|
34
|
+
--timestamp-field created_at \
|
|
35
|
+
--deep \
|
|
36
|
+
--out exposure-report.html
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
One line, if your shell mangles the backslash continuations:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv run maskflow scan jsonl packages/maskflow-cli/examples/sample-llm-traffic.jsonl --field 'messages[].content' --provider-field provider --service-field model --timestamp-field created_at --deep --out exposure-report.html
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `--field 'messages[].content'` — pull the text out of each record (the `[]`
|
|
46
|
+
means "every message"). **Quote it** — `[]` is a glob character in most
|
|
47
|
+
shells, so an unquoted `messages[].content` fails with "no matches found".
|
|
48
|
+
- `--provider-field` / `--service-field` / `--timestamp-field` — map the
|
|
49
|
+
record's metadata columns so the report can break exposure down by
|
|
50
|
+
provider, model, and time.
|
|
51
|
+
- `--deep` — run the full pipeline (names and addresses included) over every
|
|
52
|
+
record. Fine here because the file is tiny; on a real multi-GB corpus you
|
|
53
|
+
would drop `--deep` and let the NER pass run on a sample.
|
|
54
|
+
- `--out` — where to write the report. `--format json` or `--format csv`
|
|
55
|
+
give you the same data as structured output instead.
|
|
56
|
+
|
|
57
|
+
Open `exposure-report.html` in a browser. It is a single self-contained file
|
|
58
|
+
— no network, no external assets — so it prints cleanly and can be emailed
|
|
59
|
+
as-is.
|
|
60
|
+
|
|
61
|
+
## What the report shows
|
|
62
|
+
|
|
63
|
+
For this sample you should see roughly:
|
|
64
|
+
|
|
65
|
+
- **~165 PII instances** reached three providers (openai, anthropic,
|
|
66
|
+
google), across **~49 of 60** records.
|
|
67
|
+
- A breakdown by entity type — Aadhaar, PAN, passport, and credit card land
|
|
68
|
+
in the **Critical** severity band; addresses, driving licences, DOB, UPI
|
|
69
|
+
in **High**; names, mobiles, email, GSTIN in **Medium**; IFSC in **Low**.
|
|
70
|
+
- A per-day time series and a per-provider / per-model split.
|
|
71
|
+
- Expandable **masked** example contexts under each severity row — the
|
|
72
|
+
values are shown only as typed placeholders like `<AADHAAR_1>`, never
|
|
73
|
+
raw.
|
|
74
|
+
- An **Appendix A** slot for the DPDP Rule 6 mapping.
|
|
75
|
+
|
|
76
|
+
## Regenerate
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uv run python packages/maskflow-cli/examples/generate_sample.py
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Deterministic (fixed seed), so the committed file only changes when the
|
|
83
|
+
generator does.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Regenerates examples/sample-llm-traffic.jsonl.
|
|
2
|
+
|
|
3
|
+
EVERYTHING THIS PRODUCES IS SYNTHETIC. Identifiers are generated to be
|
|
4
|
+
checksum-/format-valid but are drawn uniformly at random within that shape
|
|
5
|
+
and are never looked up against, or taken from, any real registry -- see
|
|
6
|
+
`bench/indiapii/generator/identifiers.py`. Names, emails, and addresses are
|
|
7
|
+
assembled from the same synthetic pools. Do not treat any value here as
|
|
8
|
+
belonging to a real person or organisation.
|
|
9
|
+
|
|
10
|
+
Run from the repo root: uv run python packages/maskflow-cli/examples/generate_sample.py
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import random
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# The synthetic-identifier generators live in the repo's bench/ tree.
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
|
|
22
|
+
|
|
23
|
+
from bench.indiapii.generator import identifiers as gen # noqa: E402
|
|
24
|
+
|
|
25
|
+
OUT = Path(__file__).parent / "sample-llm-traffic.jsonl"
|
|
26
|
+
SEED = 20260513 # DPDP enforcement date -- deterministic output
|
|
27
|
+
N = 60
|
|
28
|
+
|
|
29
|
+
_PROVIDERS = [
|
|
30
|
+
("openai", "gpt-4o"),
|
|
31
|
+
("openai", "gpt-4o-mini"),
|
|
32
|
+
("anthropic", "claude-sonnet-4"),
|
|
33
|
+
("anthropic", "claude-3-5-haiku"),
|
|
34
|
+
("google", "gemini-1.5-pro"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# ~1 in 4 records is a clean support query with no PII, so the sample report
|
|
38
|
+
# shows a realistic "N of M records contained PII" ratio.
|
|
39
|
+
_CLEAN = [
|
|
40
|
+
"How do I reset my password?",
|
|
41
|
+
"What are your business hours on public holidays?",
|
|
42
|
+
"Summarise the attached refund policy in three bullet points.",
|
|
43
|
+
"Draft a polite reminder email about an overdue invoice (no names).",
|
|
44
|
+
"What's the difference between IMPS and NEFT?",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _pii_prompt(rng: random.Random) -> str:
|
|
49
|
+
name = gen.generate_person_name(rng)
|
|
50
|
+
kind = rng.randrange(6)
|
|
51
|
+
if kind == 0:
|
|
52
|
+
dob = f"19{rng.randint(60, 99)}-{rng.randint(1, 12):02d}-{rng.randint(1, 28):02d}"
|
|
53
|
+
return (
|
|
54
|
+
f"Run a KYC check for {name}. Aadhaar {gen.generate_aadhaar(rng)}, "
|
|
55
|
+
f"PAN {gen.generate_pan(rng)}, DOB {dob}."
|
|
56
|
+
)
|
|
57
|
+
if kind == 1:
|
|
58
|
+
return (
|
|
59
|
+
f"Customer {name} ({gen.generate_indian_mobile(rng)}) wants a refund to "
|
|
60
|
+
f"UPI {gen.generate_upi_vpa(rng)} -- draft the confirmation message."
|
|
61
|
+
)
|
|
62
|
+
if kind == 2:
|
|
63
|
+
return (
|
|
64
|
+
f"Vendor onboarding: GSTIN {gen.generate_gstin(rng)}, settlement account "
|
|
65
|
+
f"IFSC {gen.generate_ifsc(rng)}, contact {name.split()[0].lower()}@vendor.example."
|
|
66
|
+
)
|
|
67
|
+
if kind == 3:
|
|
68
|
+
return (
|
|
69
|
+
f"Ship the replacement unit to {name}, {gen.generate_indian_address(rng)}. "
|
|
70
|
+
f"Reachable on {gen.generate_indian_mobile(rng)}."
|
|
71
|
+
)
|
|
72
|
+
if kind == 4:
|
|
73
|
+
return (
|
|
74
|
+
f"Cardholder {name} disputes a charge on card "
|
|
75
|
+
f"{_fake_card(rng)}; summarise the case for the ops team."
|
|
76
|
+
)
|
|
77
|
+
return (
|
|
78
|
+
f"Passport {gen.generate_indian_passport(rng)} and driving licence "
|
|
79
|
+
f"{gen.generate_driving_licence(rng)} submitted by {name} for verification."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _fake_card(rng: random.Random) -> str:
|
|
84
|
+
# A Luhn-valid 16-digit number in a test BIN range (4242...) -- same
|
|
85
|
+
# "valid shape, not a real card" discipline as the other identifiers.
|
|
86
|
+
digits = [4, 2, 4, 2] + [rng.randrange(10) for _ in range(11)]
|
|
87
|
+
checksum = 0
|
|
88
|
+
for idx, d in enumerate(reversed(digits)):
|
|
89
|
+
d = d * 2 if idx % 2 == 0 else d
|
|
90
|
+
checksum += d - 9 if d > 9 else d
|
|
91
|
+
digits.append((10 - checksum % 10) % 10)
|
|
92
|
+
s = "".join(map(str, digits))
|
|
93
|
+
return f"{s[0:4]} {s[4:8]} {s[8:12]} {s[12:16]}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main() -> None:
|
|
97
|
+
rng = random.Random(SEED)
|
|
98
|
+
rows: list[dict] = []
|
|
99
|
+
for i in range(N):
|
|
100
|
+
provider, model = _PROVIDERS[i % len(_PROVIDERS)]
|
|
101
|
+
month = 1 + (i * 7) % 8 # spread Jan..Aug 2026
|
|
102
|
+
day = 1 + (i * 13) % 27
|
|
103
|
+
ts = f"2026-{month:02d}-{day:02d}T{9 + i % 9:02d}:{(i * 17) % 60:02d}:00Z"
|
|
104
|
+
content = rng.choice(_CLEAN) if i % 4 == 0 else _pii_prompt(rng)
|
|
105
|
+
rows.append(
|
|
106
|
+
{
|
|
107
|
+
"request_id": f"req_{i:04d}",
|
|
108
|
+
"provider": provider,
|
|
109
|
+
"model": model,
|
|
110
|
+
"created_at": ts,
|
|
111
|
+
"messages": [{"role": "user", "content": content}],
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
OUT.write_text("\n".join(json.dumps(r) for r in rows) + "\n", encoding="utf-8")
|
|
115
|
+
print(f"wrote {len(rows)} records -> {OUT}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{"request_id": "req_0000", "provider": "openai", "model": "gpt-4o", "created_at": "2026-01-01T09:00:00Z", "messages": [{"role": "user", "content": "What are your business hours on public holidays?"}]}
|
|
2
|
+
{"request_id": "req_0001", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-08-14T10:17:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 13BVNCH2630I6ZD, settlement account IFSC KBHB0GLR7ED, contact rameeni@vendor.example."}]}
|
|
3
|
+
{"request_id": "req_0002", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-07-27T11:34:00Z", "messages": [{"role": "user", "content": "Cardholder Manosha disputes a charge on card 4242 5486 7637 3085; summarise the case for the ops team."}]}
|
|
4
|
+
{"request_id": "req_0003", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-06-13T12:51:00Z", "messages": [{"role": "user", "content": "Customer Chaitr Sarvadharmamaya (+91-8958297726) wants a refund to UPI e1ypcn9@okicici -- draft the confirmation message."}]}
|
|
5
|
+
{"request_id": "req_0004", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-05-26T13:08:00Z", "messages": [{"role": "user", "content": "What's the difference between IMPS and NEFT?"}]}
|
|
6
|
+
{"request_id": "req_0005", "provider": "openai", "model": "gpt-4o", "created_at": "2026-04-12T14:25:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Rajasi Bhishada. Aadhaar 9294 4857 5420, PAN ZHSBN5203Q, DOB 1985-03-20."}]}
|
|
7
|
+
{"request_id": "req_0006", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-03-25T15:42:00Z", "messages": [{"role": "user", "content": "Customer Arunodhayen (+916643314760) wants a refund to UPI g5srz89yle2wb@waicici -- draft the confirmation message."}]}
|
|
8
|
+
{"request_id": "req_0007", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-02-11T16:59:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Lingam Arieon, Door No. 623, Kural Layout, Thiruvananthapuram, Chhattisgarh. Reachable on +91-8763582776."}]}
|
|
9
|
+
{"request_id": "req_0008", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-01-24T17:16:00Z", "messages": [{"role": "user", "content": "Draft a polite reminder email about an overdue invoice (no names)."}]}
|
|
10
|
+
{"request_id": "req_0009", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-08-10T09:33:00Z", "messages": [{"role": "user", "content": "Customer Shrikalasundar Roderic (+91-8101485724) wants a refund to UPI jqp72@yesbank -- draft the confirmation message."}]}
|
|
11
|
+
{"request_id": "req_0010", "provider": "openai", "model": "gpt-4o", "created_at": "2026-07-23T10:50:00Z", "messages": [{"role": "user", "content": "Passport W96 27182 and driving licence HP64-2016-6746731 submitted by Ratirat for verification."}]}
|
|
12
|
+
{"request_id": "req_0011", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-06-09T11:07:00Z", "messages": [{"role": "user", "content": "Passport V20 72133 and driving licence TS64 2022 8000356 submitted by Wilde Tarika for verification."}]}
|
|
13
|
+
{"request_id": "req_0012", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-05-22T12:24:00Z", "messages": [{"role": "user", "content": "Draft a polite reminder email about an overdue invoice (no names)."}]}
|
|
14
|
+
{"request_id": "req_0013", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-04-08T13:41:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Aroop, Phase 984A, Chanchalarcha Vihar, Madavaram, West Bengal. Reachable on +916652213920."}]}
|
|
15
|
+
{"request_id": "req_0014", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-03-21T14:58:00Z", "messages": [{"role": "user", "content": "Cardholder Kunjal disputes a charge on card 4242 1010 0295 3582; summarise the case for the ops team."}]}
|
|
16
|
+
{"request_id": "req_0015", "provider": "openai", "model": "gpt-4o", "created_at": "2026-02-07T15:15:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Vrithika, Sector 794B, Joshnika Nagar, Veraval, Daman And Diu. Reachable on +91 6734853374."}]}
|
|
17
|
+
{"request_id": "req_0016", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-01-20T16:32:00Z", "messages": [{"role": "user", "content": "Draft a polite reminder email about an overdue invoice (no names)."}]}
|
|
18
|
+
{"request_id": "req_0017", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-08-06T17:49:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Zhaamileey, Plot 454B, Soruban Nagar, Buxar, Andhra Pradesh. Reachable on 08089671059."}]}
|
|
19
|
+
{"request_id": "req_0018", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-07-19T09:06:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Sammbhu. Aadhaar 527254856909, PAN BQHJS2680F, DOB 1966-04-11."}]}
|
|
20
|
+
{"request_id": "req_0019", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-06-05T10:23:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Averie Arhatasamhita. Aadhaar 6658 5114 2622, PAN RLLBT8160F, DOB 1962-12-12."}]}
|
|
21
|
+
{"request_id": "req_0020", "provider": "openai", "model": "gpt-4o", "created_at": "2026-05-18T11:40:00Z", "messages": [{"role": "user", "content": "Summarise the attached refund policy in three bullet points."}]}
|
|
22
|
+
{"request_id": "req_0021", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-04-04T12:57:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Mahasarasvat, Block 359, Sudurlabha Colony, Vasai-Virar, Himachal Pradesh. Reachable on +916457617906."}]}
|
|
23
|
+
{"request_id": "req_0022", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-03-17T13:14:00Z", "messages": [{"role": "user", "content": "Cardholder Mahendrakumar Rajamuthu disputes a charge on card 4242 7405 5329 7491; summarise the case for the ops team."}]}
|
|
24
|
+
{"request_id": "req_0023", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-02-03T14:31:00Z", "messages": [{"role": "user", "content": "Customer Chabla (06592230346) wants a refund to UPI tmo14upg88p@ybl -- draft the confirmation message."}]}
|
|
25
|
+
{"request_id": "req_0024", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-01-16T15:48:00Z", "messages": [{"role": "user", "content": "What are your business hours on public holidays?"}]}
|
|
26
|
+
{"request_id": "req_0025", "provider": "openai", "model": "gpt-4o", "created_at": "2026-08-02T16:05:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 22FEAFD2442R8ZY, settlement account IFSC BCEY00QLUND, contact kushanthi@vendor.example."}]}
|
|
27
|
+
{"request_id": "req_0026", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-07-15T17:22:00Z", "messages": [{"role": "user", "content": "Passport V80 84042 and driving licence TR37 1991 9387627 submitted by Winry Iyanharan for verification."}]}
|
|
28
|
+
{"request_id": "req_0027", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-06-01T09:39:00Z", "messages": [{"role": "user", "content": "Passport V43 67482 and driving licence KL78-2008-4950584 submitted by Lilliann Zoella for verification."}]}
|
|
29
|
+
{"request_id": "req_0028", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-05-14T10:56:00Z", "messages": [{"role": "user", "content": "Draft a polite reminder email about an overdue invoice (no names)."}]}
|
|
30
|
+
{"request_id": "req_0029", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-04-27T11:13:00Z", "messages": [{"role": "user", "content": "Cardholder Trulie disputes a charge on card 4242 5456 8060 8480; summarise the case for the ops team."}]}
|
|
31
|
+
{"request_id": "req_0030", "provider": "openai", "model": "gpt-4o", "created_at": "2026-03-13T12:30:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Bhajuna Ruchel. Aadhaar 4154 6145 8082, PAN QEPPV8191N, DOB 1984-11-12."}]}
|
|
32
|
+
{"request_id": "req_0031", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-02-26T13:47:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Lisaa Dio, Phase 412B, Hulk Colony, Rourkela, Goa. Reachable on +918083496965."}]}
|
|
33
|
+
{"request_id": "req_0032", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-01-12T14:04:00Z", "messages": [{"role": "user", "content": "How do I reset my password?"}]}
|
|
34
|
+
{"request_id": "req_0033", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-08-25T15:21:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 22PBYFG4129B7ZX, settlement account IFSC ABBL0UOJH9H, contact yoganandh@vendor.example."}]}
|
|
35
|
+
{"request_id": "req_0034", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-07-11T16:38:00Z", "messages": [{"role": "user", "content": "Customer Indujan Brihadgataye (+919177090793) wants a refund to UPI 0bkfzpxdgas@ybl -- draft the confirmation message."}]}
|
|
36
|
+
{"request_id": "req_0035", "provider": "openai", "model": "gpt-4o", "created_at": "2026-06-24T17:55:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Ekaikanishthita, Flat 641, Devion Puram, Rajkot, Ladakh. Reachable on +91 9825034337."}]}
|
|
37
|
+
{"request_id": "req_0036", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-05-10T09:12:00Z", "messages": [{"role": "user", "content": "What's the difference between IMPS and NEFT?"}]}
|
|
38
|
+
{"request_id": "req_0037", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-04-23T10:29:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 07OHBFX7274J6ZS, settlement account IFSC IOBA0V3NCRO, contact nikatha@vendor.example."}]}
|
|
39
|
+
{"request_id": "req_0038", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-03-09T11:46:00Z", "messages": [{"role": "user", "content": "Customer Daryan (+91 7616038330) wants a refund to UPI ebkz1tor9@unionbank -- draft the confirmation message."}]}
|
|
40
|
+
{"request_id": "req_0039", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-02-22T12:03:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Vrishabhapriya Asikundagata, Block 63B, Yamit Vihar, Haldwani-cum-Kathgodam, Gujarat. Reachable on +917150873370."}]}
|
|
41
|
+
{"request_id": "req_0040", "provider": "openai", "model": "gpt-4o", "created_at": "2026-01-08T13:20:00Z", "messages": [{"role": "user", "content": "What are your business hours on public holidays?"}]}
|
|
42
|
+
{"request_id": "req_0041", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-08-21T14:37:00Z", "messages": [{"role": "user", "content": "Cardholder Svahsthita Ameliah disputes a charge on card 4242 4252 1430 9790; summarise the case for the ops team."}]}
|
|
43
|
+
{"request_id": "req_0042", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-07-07T15:54:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Gunash Bhanusri. Aadhaar 4556-7185-1809, PAN PPVJZ8421L, DOB 1973-02-02."}]}
|
|
44
|
+
{"request_id": "req_0043", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-06-20T16:11:00Z", "messages": [{"role": "user", "content": "Cardholder Sangiliyen Ubaithulla disputes a charge on card 4242 2049 8682 8117; summarise the case for the ops team."}]}
|
|
45
|
+
{"request_id": "req_0044", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-05-06T17:28:00Z", "messages": [{"role": "user", "content": "How do I reset my password?"}]}
|
|
46
|
+
{"request_id": "req_0045", "provider": "openai", "model": "gpt-4o", "created_at": "2026-04-19T09:45:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 27ZCAFM7450J8Z5, settlement account IFSC BKDN07S3Y96, contact rahamathulla@vendor.example."}]}
|
|
47
|
+
{"request_id": "req_0046", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-03-05T10:02:00Z", "messages": [{"role": "user", "content": "Vendor onboarding: GSTIN 24HFEJU6417Q5ZU, settlement account IFSC NKGS0U42NW6, contact gunagarbha@vendor.example."}]}
|
|
48
|
+
{"request_id": "req_0047", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-02-18T11:19:00Z", "messages": [{"role": "user", "content": "Cardholder Anku Vikrit disputes a charge on card 4242 1722 4695 2466; summarise the case for the ops team."}]}
|
|
49
|
+
{"request_id": "req_0048", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-01-04T12:36:00Z", "messages": [{"role": "user", "content": "What's the difference between IMPS and NEFT?"}]}
|
|
50
|
+
{"request_id": "req_0049", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-08-17T13:53:00Z", "messages": [{"role": "user", "content": "Run a KYC check for Shrutapapaghn Isewar. Aadhaar 8769-8005-8680, PAN MJZBF9014V, DOB 1962-06-06."}]}
|
|
51
|
+
{"request_id": "req_0050", "provider": "openai", "model": "gpt-4o", "created_at": "2026-07-03T14:10:00Z", "messages": [{"role": "user", "content": "Customer Harout Amaru (+91 7156684675) wants a refund to UPI jtik@baroda -- draft the confirmation message."}]}
|
|
52
|
+
{"request_id": "req_0051", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-06-16T15:27:00Z", "messages": [{"role": "user", "content": "Passport H9867332 and driving licence SK21 1997 1695474 submitted by Msk for verification."}]}
|
|
53
|
+
{"request_id": "req_0052", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-05-02T16:44:00Z", "messages": [{"role": "user", "content": "What's the difference between IMPS and NEFT?"}]}
|
|
54
|
+
{"request_id": "req_0053", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-04-15T17:01:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Bhutikrite, H.No. 863A, Adris Vihar, Buxar, Jharkhand. Reachable on +91-9157079078."}]}
|
|
55
|
+
{"request_id": "req_0054", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-03-01T09:18:00Z", "messages": [{"role": "user", "content": "Cardholder Akish disputes a charge on card 4242 5207 2782 7518; summarise the case for the ops team."}]}
|
|
56
|
+
{"request_id": "req_0055", "provider": "openai", "model": "gpt-4o", "created_at": "2026-02-14T10:35:00Z", "messages": [{"role": "user", "content": "Ship the replacement unit to Jeyalatha, Block 175, Kiro Puram, Raurkela Industrial Township, Nagaland. Reachable on +919624990544."}]}
|
|
57
|
+
{"request_id": "req_0056", "provider": "openai", "model": "gpt-4o-mini", "created_at": "2026-01-27T11:52:00Z", "messages": [{"role": "user", "content": "What are your business hours on public holidays?"}]}
|
|
58
|
+
{"request_id": "req_0057", "provider": "anthropic", "model": "claude-sonnet-4", "created_at": "2026-08-13T12:09:00Z", "messages": [{"role": "user", "content": "Cardholder Kumarasan disputes a charge on card 4242 7360 9493 5821; summarise the case for the ops team."}]}
|
|
59
|
+
{"request_id": "req_0058", "provider": "anthropic", "model": "claude-3-5-haiku", "created_at": "2026-07-26T13:26:00Z", "messages": [{"role": "user", "content": "Cardholder Maryclaire disputes a charge on card 4242 7437 1044 5565; summarise the case for the ops team."}]}
|
|
60
|
+
{"request_id": "req_0059", "provider": "google", "model": "gemini-1.5-pro", "created_at": "2026-06-12T14:43:00Z", "messages": [{"role": "user", "content": "Cardholder Printhaapan disputes a charge on card 4242 2891 4265 2444; summarise the case for the ops team."}]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Packaging `maskflow`
|
|
2
|
+
|
|
3
|
+
How the CLI is distributed, and how each artifact is built.
|
|
4
|
+
|
|
5
|
+
| channel | what you get | NER pass (names/addresses/DOB)? |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| **pipx / pip** | `pip install maskflow-cli` (`[s3]` / `[postgres]` extras) | Yes, with `python -m spacy download en_core_web_sm` |
|
|
8
|
+
| **Docker** — `ghcr.io/maskflow/cli` | full CLI, spaCy + model baked in | Yes, out of the box |
|
|
9
|
+
| **Standalone binary** — GitHub Releases | single ~15 MB file, no Python needed | **No** — pattern/checksum pass only |
|
|
10
|
+
| **GitHub Action** — [`packaging/scan-action`](../../../packaging/scan-action/) | `maskflow scan` in CI, report as an artifact | Yes (downloads the model) |
|
|
11
|
+
|
|
12
|
+
## pipx
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pipx install maskflow-cli
|
|
16
|
+
# or one-off:
|
|
17
|
+
pipx run maskflow-cli scan jsonl requests.jsonl --field 'messages[].content'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`maskflow-cli` publishes to PyPI from `.github/workflows/release-cli.yml`
|
|
21
|
+
on a `cli-py-v*` tag (trusted publishing, no stored token).
|
|
22
|
+
|
|
23
|
+
## Docker
|
|
24
|
+
|
|
25
|
+
`Dockerfile` in the package root. Built and pushed to
|
|
26
|
+
`ghcr.io/maskflow/cli:<version>` and `:latest` by the same release
|
|
27
|
+
workflow, `linux/amd64` + `linux/arm64`.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
docker run --rm -v "$PWD:/work" ghcr.io/maskflow/cli \
|
|
31
|
+
scan jsonl /work/requests.jsonl --field 'messages[].content' \
|
|
32
|
+
--deep -o /work/exposure-report.html
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Standalone binary
|
|
36
|
+
|
|
37
|
+
`maskflow.spec` + `_entry.py` here, built with PyInstaller by the release
|
|
38
|
+
workflow's `binaries` matrix (linux/macos/windows) and attached to the
|
|
39
|
+
GitHub Release.
|
|
40
|
+
|
|
41
|
+
**The binary deliberately excludes spaCy** — bundling it and the model
|
|
42
|
+
turns ~15 MB into ~1 GB and is fragile across the three OSes. So the binary
|
|
43
|
+
runs the **pattern/checksum pass only**: Aadhaar, PAN, GSTIN, UPI, IFSC,
|
|
44
|
+
credit cards, email, phones, the Indian gazetteer-based name/address
|
|
45
|
+
layers, etc. It does *not* run the generic spaCy NER pass; `--deep` exits
|
|
46
|
+
with a message pointing at the pip / Docker install. Reports it generates
|
|
47
|
+
still contain no raw PII (excerpt dates are scrubbed regardless of NER).
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pyinstaller --clean --noconfirm packages/maskflow-cli/packaging/maskflow.spec
|
|
51
|
+
./dist/maskflow scan jsonl requests.jsonl --field 'messages[].content'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`_entry.py` calls `multiprocessing.freeze_support()` first — without it a
|
|
55
|
+
`scan --workers >1` pool re-launches the frozen binary with
|
|
56
|
+
multiprocessing's own argv and Typer aborts on the unknown `-B`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""PyInstaller entry point for the standalone `maskflow` binary.
|
|
2
|
+
|
|
3
|
+
A thin module (not a console-script shim) so the spec has a concrete file
|
|
4
|
+
to analyse. Imports the packs explicitly -- in a frozen build the
|
|
5
|
+
`"maskflow.recognizers"` entry points are not always discoverable, but the
|
|
6
|
+
direct import side effect always registers the recognizers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import multiprocessing
|
|
12
|
+
|
|
13
|
+
import maskflow_pack_india # noqa: F401
|
|
14
|
+
import maskflow_pack_intl # noqa: F401
|
|
15
|
+
from maskflow_cli.app import main
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
# MUST be first: when `scan --workers >1` spawns a pool, PyInstaller
|
|
19
|
+
# re-launches this same binary as the worker with multiprocessing's own
|
|
20
|
+
# argv (`-B -S -c ...`). freeze_support() intercepts that, runs the
|
|
21
|
+
# worker, and exits -- without it Typer sees `-B` and aborts.
|
|
22
|
+
multiprocessing.freeze_support()
|
|
23
|
+
main()
|