ciphergate-cli 0.7.0__py3-none-any.whl

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.
Files changed (74) hide show
  1. ciphergate/__init__.py +95 -0
  2. ciphergate/__main__.py +6 -0
  3. ciphergate/cli.py +832 -0
  4. ciphergate/config.py +50 -0
  5. ciphergate/constants.py +274 -0
  6. ciphergate/detectors/__init__.py +5 -0
  7. ciphergate/detectors/base.py +53 -0
  8. ciphergate/detectors/custom_regex.py +37 -0
  9. ciphergate/detectors/dates.py +92 -0
  10. ciphergate/detectors/email.py +56 -0
  11. ciphergate/detectors/field_names.py +220 -0
  12. ciphergate/detectors/ip_address.py +85 -0
  13. ciphergate/detectors/network.py +42 -0
  14. ciphergate/detectors/payment_card.py +77 -0
  15. ciphergate/detectors/phone.py +67 -0
  16. ciphergate/detectors/presidio_ner.py +112 -0
  17. ciphergate/detectors/regex_safety.py +40 -0
  18. ciphergate/detectors/registry.py +194 -0
  19. ciphergate/detectors/secrets.py +130 -0
  20. ciphergate/errors.py +119 -0
  21. ciphergate/models/__init__.py +1 -0
  22. ciphergate/models/context.py +69 -0
  23. ciphergate/models/finding.py +108 -0
  24. ciphergate/models/policy.py +402 -0
  25. ciphergate/models/report.py +115 -0
  26. ciphergate/pipeline/__init__.py +1 -0
  27. ciphergate/pipeline/atomic_output.py +143 -0
  28. ciphergate/pipeline/batch.py +533 -0
  29. ciphergate/pipeline/inspector.py +282 -0
  30. ciphergate/pipeline/paths.py +98 -0
  31. ciphergate/pipeline/planner.py +192 -0
  32. ciphergate/pipeline/rescan.py +120 -0
  33. ciphergate/pipeline/sanitizer.py +392 -0
  34. ciphergate/pipeline/stdio.py +173 -0
  35. ciphergate/pipeline/streams.py +152 -0
  36. ciphergate/policy/__init__.py +26 -0
  37. ciphergate/policy/context.py +203 -0
  38. ciphergate/policy/engine.py +194 -0
  39. ciphergate/policy/loader.py +116 -0
  40. ciphergate/policy/precedence.py +149 -0
  41. ciphergate/policy/validator.py +632 -0
  42. ciphergate/py.typed +0 -0
  43. ciphergate/readers/__init__.py +113 -0
  44. ciphergate/readers/accesslog_reader.py +244 -0
  45. ciphergate/readers/base.py +131 -0
  46. ciphergate/readers/csv_reader.py +241 -0
  47. ciphergate/readers/json_reader.py +184 -0
  48. ciphergate/readers/jsonl_reader.py +97 -0
  49. ciphergate/readers/jsonlog_reader.py +178 -0
  50. ciphergate/readers/line_structure.py +125 -0
  51. ciphergate/readers/logfmt_reader.py +100 -0
  52. ciphergate/readers/stacktrace_reader.py +103 -0
  53. ciphergate/readers/syslog_reader.py +309 -0
  54. ciphergate/readers/text_reader.py +59 -0
  55. ciphergate/reporting/__init__.py +1 -0
  56. ciphergate/reporting/audit.py +85 -0
  57. ciphergate/reporting/safe_text.py +71 -0
  58. ciphergate/reporting/serializer.py +44 -0
  59. ciphergate/reporting/terminal.py +147 -0
  60. ciphergate/templates/starter_policy.yml +77 -0
  61. ciphergate/transforms/__init__.py +6 -0
  62. ciphergate/transforms/date_shift.py +79 -0
  63. ciphergate/transforms/generalize.py +107 -0
  64. ciphergate/transforms/keys.py +177 -0
  65. ciphergate/transforms/pseudonymize.py +113 -0
  66. ciphergate/transforms/redact.py +9 -0
  67. ciphergate/transforms/registry.py +201 -0
  68. ciphergate/transforms/replace.py +16 -0
  69. ciphergate_cli-0.7.0.dist-info/METADATA +531 -0
  70. ciphergate_cli-0.7.0.dist-info/RECORD +74 -0
  71. ciphergate_cli-0.7.0.dist-info/WHEEL +4 -0
  72. ciphergate_cli-0.7.0.dist-info/entry_points.txt +2 -0
  73. ciphergate_cli-0.7.0.dist-info/licenses/LICENSE +202 -0
  74. ciphergate_cli-0.7.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,531 @@
1
+ Metadata-Version: 2.5
2
+ Name: ciphergate-cli
3
+ Version: 0.7.0
4
+ Summary: Local-first, environment- and destination-aware inspection and sanitization of logs and test datasets.
5
+ Project-URL: Homepage, https://github.com/aashishkumar-tech/ciphergate
6
+ Project-URL: Source, https://github.com/aashishkumar-tech/ciphergate
7
+ Project-URL: Issues, https://github.com/aashishkumar-tech/ciphergate/issues
8
+ Project-URL: Changelog, https://github.com/aashishkumar-tech/ciphergate/blob/main/CHANGELOG.md
9
+ Author: CipherGate maintainers
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ License-File: NOTICE
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: pydantic<3.0,>=2.6
25
+ Requires-Dist: pyyaml<7.0,>=6.0.1
26
+ Requires-Dist: typer<1.0,>=0.19
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.2; extra == 'dev'
29
+ Requires-Dist: hypothesis>=6.100; extra == 'dev'
30
+ Requires-Dist: mypy>=1.10; extra == 'dev'
31
+ Requires-Dist: pre-commit>=3.5; extra == 'dev'
32
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
33
+ Requires-Dist: pytest>=8.0; extra == 'dev'
34
+ Requires-Dist: ruff>=0.6; extra == 'dev'
35
+ Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
36
+ Provides-Extra: ner
37
+ Requires-Dist: presidio-analyzer<3,>=2.2; extra == 'ner'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # CipherGate
41
+
42
+ **Inspect and sanitize logs and test datasets before they leave their environment** — with
43
+ policies that depend on where the data came from (DEV/STG/PRD) *and* where it is going (local
44
+ debugging, internal monitoring, an external vendor or AI assistant, a public repository).
45
+
46
+ Teams often hand-redact logs or keep ad-hoc scripts before pasting data into an AI assistant or a
47
+ vendor ticket. CipherGate replaces that with a local, explainable, scriptable gate:
48
+
49
+ ```
50
+ inspect -> plan -> sanitize --apply -> rescan
51
+ ```
52
+
53
+ > **Status: 0.7.0 (alpha).** No detector finds every sensitive value. A passing run means the
54
+ > configured processing completed under the selected policy; it is **not** proof that the output
55
+ > contains no sensitive information, and it is not a compliance (GDPR, HIPAA, PCI DSS, ...) or
56
+ > anonymization guarantee. Policy templates must be reviewed before use.
57
+
58
+ ## What it is / what it is not
59
+
60
+ | It is | It is not |
61
+ |---|---|
62
+ | A local CLI + Python library; no network calls, accounts or telemetry | A hosted service, dashboard, or AI classifier |
63
+ | Deterministic detectors + a separate, testable policy engine | A guarantee that all PII/secrets are found |
64
+ | Environment- **and** destination-aware rules with fail-closed defaults | Proof of where data really came from (context is *declared*) |
65
+ | Redaction, replacement, keyed HMAC pseudonyms, generalization, date shifting, drops | Reversible tokenization (no token vault in this release) |
66
+ | Atomic, no-overwrite output with a residual rescan gate | A deployment tool or cloud integration |
67
+ | Privacy-safe JSON/text reports with stable reason codes and exit codes | A legal/compliance determination |
68
+
69
+ ## Features (implemented and tested)
70
+
71
+ - **Formats:** CSV (quoted fields, embedded newlines, BOM, CRLF, configurable delimiter), JSON
72
+ (whole document, JSON Pointer paths), JSONL/NDJSON (streamed), plain text/log (line based,
73
+ multi-line PEM private-key blocks), logfmt (`--format logfmt`: `key=value` log lines), JSON
74
+ in log lines (`--format jsonlog`), nginx/Apache access logs (`--format accesslog`),
75
+ syslog/CEF (`--format syslog`) and logs with Python/JVM/Node stack traces
76
+ (`--format stacktrace`), see below.
77
+ - **Detectors (local, deterministic):** field-name heuristics, email, phone (conservative
78
+ confidence), IPv4/IPv6 (stdlib-validated; private addresses as `network.telemetry`), Luhn card
79
+ numbers, dates (DOB only with field/context evidence), credentials/secrets (PEM, bearer/basic,
80
+ AWS/GitHub/Slack/Stripe-style/Google keys, JWTs, URL passwords, connection strings,
81
+ `password=`-style assignments), internal hostnames, policy field classifications, and custom
82
+ regexes with safety checks. See [docs/detector-coverage.md](docs/detector-coverage.md).
83
+ - **Policy engine:** strict versioned YAML schema, deterministic precedence, conflict rejection,
84
+ credential hard constraints, environment/destination guardrails, fail-closed fallback.
85
+ - **Actions:** `allow`, `redact`, `replace`, `pseudonymize` (HMAC-SHA-256, scoped, versioned),
86
+ `generalize` (year, year-month, age bands, coordinate rounding, numeric bins), `date_shift`
87
+ (keyed, per-dataset or per-entity), `drop_field`, `drop_record`, `block`, `review`.
88
+ - **Safe output:** two-pass processing, temp file + fsync + atomic publish, overwrite only with
89
+ `--force`, input/output alias protection, input-change detection, cleanup on failure/Ctrl-C.
90
+ - **Reports:** counts by category/detector/action/reason/rule, safe locations, effective context
91
+ and its source, gates, key *status* (never key material), no raw values or fingerprints.
92
+
93
+ ## Install
94
+
95
+ Requires Python 3.11+.
96
+
97
+ **From PyPI** (the package is named `ciphergate-cli`; the command and `import ciphergate` stay
98
+ `ciphergate`):
99
+
100
+ ```bash
101
+ python -m pip install ciphergate-cli
102
+ ```
103
+
104
+ Do not run `pip install ciphergate`: that PyPI name belongs to an unrelated project. PyPI uploads
105
+ start with 0.7.0. Older versions are only on
106
+ [GitHub releases](https://github.com/aashishkumar-tech/ciphergate/releases), as a wheel, an sdist
107
+ and `SHA256SUMS`.
108
+
109
+ **From source:**
110
+
111
+ ```bash
112
+ python -m venv .venv
113
+ # Windows: .venv\Scripts\activate macOS/Linux: source .venv/bin/activate
114
+ python -m pip install -e ".[dev]" # or: python -m pip install .
115
+ ciphergate --version
116
+ ```
117
+
118
+ Runtime dependencies: `typer`, `pydantic`, `PyYAML`.
119
+
120
+ ## Quick start (synthetic examples)
121
+
122
+ ```bash
123
+ # 1. Start from a template (refuses to overwrite without --force) and validate it.
124
+ ciphergate init --output ciphergate-policy.yml
125
+ ciphergate validate-policy ciphergate-policy.yml
126
+
127
+ # 2. Inspect: never modifies the input, never writes sanitized data.
128
+ ciphergate inspect examples/sample.csv --policy policies/developer_logs.yml \
129
+ --environment dev --destination local_dev --report inspection.json
130
+
131
+ # 3. Plan: preview exactly what sanitize --apply would do.
132
+ ciphergate plan examples/sample.jsonl --policy policies/external_ai.yml \
133
+ --environment prd --target-environment external --destination external_ai
134
+
135
+ # 4. Sanitize: needs --apply; pseudonymization needs a key in the environment (see below).
136
+ ciphergate sanitize examples/sample.csv --policy policies/prd_external_ai.yml \
137
+ --environment prd --target-environment external --destination external_ai \
138
+ --output build/sample.sanitized.csv --report build/sample.report.json --apply
139
+
140
+ # 5. Rescan the output (read-only). Exit 7 if gating residual findings remain.
141
+ ciphergate rescan build/sample.sanitized.csv --policy policies/prd_external_ai.yml
142
+ ```
143
+
144
+ `policies/prd_external_ai.yml` pins its context (`prd` -> `external` / `external_ai`), so the
145
+ context flags in steps 4–5 must match it or be omitted; a conflicting value fails safely.
146
+
147
+ A log containing a (fake) credential is **blocked** for the external destination — exit 1 and no
148
+ output file:
149
+
150
+ ```bash
151
+ ciphergate sanitize examples/sample.log --policy policies/prd_external_ai.yml \
152
+ --output build/sample.sanitized.log --apply # exit 1, nothing written
153
+ ```
154
+
155
+ Expected sanitized outputs for `policies/developer_logs.yml` (`dev` -> `local_dev`) are in
156
+ [examples/expected/](examples/expected/). More scenarios: [docs/examples.md](docs/examples.md).
157
+
158
+ ### Pseudonymization key
159
+
160
+ Keyed actions (`pseudonymize`, `date_shift`) read key material **only** from an environment
161
+ variable (default `CIPHERGATE_HMAC_KEY`; never a CLI flag, never the policy file). Minimum 32
162
+ bytes after decoding; `hex:` and `base64:` prefixes are decoded. There is no fallback key: if it
163
+ is missing or weak, `plan` reports `configuration_required` and `sanitize --apply` exits 6
164
+ without writing output.
165
+
166
+ ```bash
167
+ # bash/zsh — generate locally; avoid pasting real keys into shell history or files you commit
168
+ export CIPHERGATE_HMAC_KEY="hex:$(python -c 'import secrets; print(secrets.token_hex(32))')"
169
+ ```
170
+
171
+ ```powershell
172
+ # PowerShell
173
+ $env:CIPHERGATE_HMAC_KEY = "hex:" + (python -c "import secrets; print(secrets.token_hex(32))")
174
+ ```
175
+
176
+ Alternatively, put the key in a file and set `CIPHERGATE_HMAC_KEY_FILE` to its path (the
177
+ Docker/Kubernetes secrets convention; it works for any `key_env` name plus `_FILE`):
178
+
179
+ ```bash
180
+ umask 077 && python -c 'import secrets; print("hex:" + secrets.token_hex(32))' > ~/.ciphergate.key
181
+ export CIPHERGATE_HMAC_KEY_FILE=~/.ciphergate.key
182
+ ```
183
+
184
+ The file must be a regular file of at most 4 KiB. On Linux/macOS it must be owned by you and not
185
+ accessible to group or others (`chmod 600` or `400`), otherwise the run fails with
186
+ `key_file_invalid`; symlinks are followed and the target is checked. On Windows CipherGate
187
+ cannot check the ACL and prints a warning: restrict the file to your account yourself. Setting
188
+ both `CIPHERGATE_HMAC_KEY` and `CIPHERGATE_HMAC_KEY_FILE` is refused.
189
+
190
+ Keep the same key to get the same pseudonyms across runs; anyone with the key can link equal
191
+ values. In CI, inject it from the platform's secret store. See
192
+ [docs/security-and-privacy.md](docs/security-and-privacy.md).
193
+
194
+ ## Context: environment vs destination
195
+
196
+ | Flag | Meaning | Values |
197
+ |---|---|---|
198
+ | `--environment` (`--source-environment`) | where the data **came from** | `dev`, `stg`, `prd`, `unknown` |
199
+ | `--target-environment` | environment that will use the output | `dev`, `stg`, `prd`, `external`, `unknown` |
200
+ | `--destination` | who/what **receives** it | `local_dev`, `approved_internal`, `staging_test`, `production_monitoring`, `external_vendor`, `external_ai`, `public_repository`, `unknown` |
201
+ | `--profile` / `--use-case` | domain profile / purpose | see [docs/policy-schema.md](docs/policy-schema.md) |
202
+
203
+ Context can be pinned by a policy or supplied per run. Missing required context fails closed;
204
+ nothing defaults to `dev` or to a trusted destination. Built-in guardrails (not overridable by
205
+ rules): PRD -> non-production is blocked without a reviewed `transfers` approval; external,
206
+ unknown, missing or production targets require a strict policy with the rescan gate and enabled
207
+ credential detectors (and JSON key scanning for JSON input); strict policies send `unknown`
208
+ context to review. **Context is declared, not verified** — in CI, set it from
209
+ controlled pipeline configuration.
210
+
211
+ ## Shipped policy templates (review before use)
212
+
213
+ `dev_local`, `stg_test`, `prd_external_ai`, `developer_logs`, `test_dataset`, `external_ai`,
214
+ `healthcare_template`, `financial_template`, `local_debug`, and `multi_environment` (one policy
215
+ whose outcomes vary by environment *and* destination). All validate in CI. Templates named for
216
+ healthcare or finance are starting points, **not** HIPAA/PCI controls.
217
+
218
+ ## Commands and exit codes
219
+
220
+ `init`, `validate-policy`, `inspect`, `plan`, `sanitize` (needs `--apply`), `rescan`, `explain`
221
+ (decision for a hypothetical finding — no data needed). All support `--help`.
222
+
223
+ Common options: `--policy`, `--format csv|json|jsonl|text|logfmt|jsonlog|accesslog|syslog|stacktrace`, `--report PATH`,
224
+ `--report-format json|text` (JSON without `--report` prints only JSON on stdout),
225
+ `--max-bytes`, `--encoding` (UTF-8 default; ASCII-compatible encodings only),
226
+ `--path-display basename|redacted|full`, `--force`, `--quiet`, `--verbose`, `--no-color`,
227
+ `--debug` (frames only, never messages/values). `rescan` adds `--strict/--no-strict`.
228
+
229
+ ### Optional local NER (Presidio)
230
+
231
+ Person names and places in free text are not found by the built-in pattern detectors. An
232
+ optional detector based on [Microsoft Presidio](https://microsoft.github.io/presidio/) can add
233
+ them. It is **off by default** and not part of the base install:
234
+
235
+ ```bash
236
+ python -m pip install "ciphergate-cli[ner]"
237
+ python -m spacy download en_core_web_lg # one-time, explicit; CipherGate never downloads models
238
+ ```
239
+
240
+ ```yaml
241
+ detectors:
242
+ presidio:
243
+ enabled: true
244
+ model: en_core_web_lg # an installed spaCy model package
245
+ language: en
246
+ min_score: 0.6 # Presidio score threshold
247
+ entities: # Presidio entity -> CipherGate category (default shown)
248
+ PERSON: pii.person_name
249
+ LOCATION: location.coarse
250
+ ```
251
+
252
+ - If the extra or the model is missing, the run fails with exit 6 (`ner_not_installed` /
253
+ `ner_model_missing`) and publishes nothing. It never falls back to running without NER. The
254
+ model is checked before Presidio is imported, because Presidio's spaCy engine would otherwise
255
+ download a missing model.
256
+ - Findings use detector ID `presidio_ner` and go through the policy like any other finding, so
257
+ rules decide what happens (`match: {detectors: [presidio_ner]}` works). NER never allows a
258
+ value, so block and review rules still win.
259
+ - NER misses names and flags non-names. It is slow (every string value goes through spaCy) and
260
+ memory-hungry, and its scores are Presidio's, not calibrated probabilities. Treat it as extra
261
+ recall, not proof.
262
+
263
+ ### Directories (batch runs)
264
+
265
+ `inspect`, `plan`, `rescan` and `sanitize` accept a directory as INPUT:
266
+
267
+ ```bash
268
+ ciphergate inspect logs/ --policy policies/developer_logs.yml --environment dev \
269
+ --destination local_dev --recursive --report build/batch.json
270
+ ciphergate sanitize logs/ --policy ... --output build/logs.sanitized --apply
271
+ ```
272
+
273
+ - Files are chosen by extension (`.csv`, `.json`, `.jsonl`, `.ndjson`, `.txt`, `.log`), or all
274
+ regular files when `--format` is given. Subdirectories are processed only with
275
+ `--recursive`/`-r` (at most 32 levels). Symlinks, junctions, devices, FIFOs and other
276
+ extensions are **skipped and listed** in the report with a reason; links are never followed.
277
+ At most 1000 files by default (`--max-files N`); more fails the run (exit 4) before anything
278
+ is processed. An empty selection fails with `batch_empty` (exit 4).
279
+ - Each file is processed exactly like a single-file run, in sorted path order; a failing file
280
+ does not stop the others, so the report covers every file.
281
+ - `sanitize` needs a **new** output directory (an existing one is refused; `--force` is not
282
+ supported for directories) outside the input directory. The whole tree is built in a private
283
+ staging directory next to it and published with one rename **only if every file was
284
+ sanitized**; otherwise nothing is published and the staging directory is removed.
285
+ - The exit code is the first failing file's code in sorted order (0 if all succeeded). The JSON
286
+ report (`ciphergate.batch-report/v1`) has a summary, every file's full report and the skipped
287
+ entries. Paths are relative to INPUT; `--path-display redacted` replaces them with
288
+ `<redacted:NNNN>.ext`. `--report` must not point inside the input or output directory.
289
+ - Directory runs cannot use stdin/stdout. The same behaviour is available from Python as
290
+ `inspect_directory` / `plan_directory` / `rescan_directory` / `sanitize_directory`.
291
+
292
+ ### Structured logs (logfmt)
293
+
294
+ `--format logfmt` reads `key=value` log lines such as
295
+ `ts=2026-01-05T10:00:00Z level=info msg="login ok" user_email=alice.test@example.com`. It is
296
+ never selected from the file extension (`.log` stays `text`).
297
+
298
+ - Every line is still scanned exactly like `text`, so logfmt never finds less than text mode
299
+ (property-tested). The parse only adds field context: a finding inside a value is attributed to
300
+ that field (`field_path` `/user_email`), and field-name heuristics run on the keys, so
301
+ `customer_name="…"` or `user_id=…` are found even when the value itself looks harmless.
302
+ Policy `field_names` / `field_paths` rules therefore apply to log fields.
303
+ - Output keeps each line as it was; only the protected spans change, so quoting and free text are
304
+ preserved. `drop_field` is not supported for logfmt (it becomes `review`; a credential there
305
+ blocks).
306
+ - Keys must start with a letter or `_`; values are bare or double-quoted with backslash escapes.
307
+ A line with broken quoting gets no field context (it is still scanned as text), and lines inside
308
+ a PEM private key block are never parsed as fields. JSON embedded in log lines is not parsed
309
+ by logfmt; use `jsonlog` for that.
310
+
311
+ ### Structured logs (JSON in log lines)
312
+
313
+ `--format jsonlog` reads log lines that embed JSON objects, such as
314
+ `2026-01-05T10:00:00Z ERROR login failed {"user": {"email": "alice.test@example.com"}}`. Like
315
+ logfmt, it is never selected from the file extension.
316
+
317
+ - Every line is still scanned exactly like `text` (property-tested: jsonlog never protects fewer
318
+ characters than text mode). Each embedded object adds field context: findings inside a value
319
+ are attributed to it (`field_path` is the JSON Pointer inside the object, e.g. `/user/email`),
320
+ and field-name heuristics and policy classifications run on every key and cover the whole
321
+ value. String values and keys containing JSON escapes (`\u0040`) are also decoded and scanned.
322
+ - Output keeps the line as it was; only protected spans change. Replacements are JSON-escaped, a
323
+ replaced number/boolean/null becomes a JSON string, and every embedded object is re-parsed
324
+ after rewriting; if one would no longer be valid JSON, the run fails and nothing is published.
325
+ A match that covers only JSON punctuation (for example a credential assignment right before an
326
+ object) protects every value of that object.
327
+ - Only objects (`{...}`) are recognised, at most 16 start positions per line, nested at most
328
+ 64 levels (the JSON depth limit). Text that is not valid JSON, and lines inside a PEM private
329
+ key block, get no field context (they are still scanned as text). `drop_field` is not
330
+ supported (it becomes `review`; a credential there blocks).
331
+
332
+ ### Access logs (nginx/Apache common and combined)
333
+
334
+ `--format accesslog` reads NCSA common/combined lines, the default layout of nginx and Apache
335
+ httpd:
336
+ `192.0.2.10 - alice [05/Jan/2026:10:00:00 +0000] "GET /p?email=a%40example.com HTTP/1.1" 200 12 "-" "curl/8"`.
337
+ It is never selected from the file extension.
338
+
339
+ - Every line is still scanned exactly like `text` (property-tested). Fields get nginx variable
340
+ names as paths: `/remote_addr`, `/ident`, `/remote_user`, `/time_local`, `/request` (or
341
+ `/request/method`, `/request/path`, `/request/protocol`), `/status`, `/body_bytes_sent`,
342
+ `/http_referer` and `/http_user_agent`. Query strings of the request and the referer become
343
+ fields as well (`/request/query/email`), so field-name heuristics and `field_names` rules
344
+ apply to parameters such as `email` or `access_token`.
345
+ - Log escapes (`\x22`, `\"`) and percent-encoding (`%40`) are decoded and the decoded value is
346
+ scanned too, so `alice.test%40example.com` is found although the raw-text scan misses it.
347
+ - Output keeps the line as it was; only protected spans change. Replacements are
348
+ percent-encoded in paths and query strings and `\xHH`-escaped elsewhere, and each rewritten
349
+ line must still parse with all its fields, or the run fails and nothing is published.
350
+ - A `-` field gets no field context. Only lines that start with the common/combined layout are
351
+ recognised: fields after the user agent (such as `$http_x_forwarded_for`), virtual-host
352
+ prefixes and custom layouts get text-mode scanning only. `drop_field` is not supported (it
353
+ becomes `review`).
354
+
355
+ ### syslog and CEF
356
+
357
+ `--format syslog` reads RFC 5424 and RFC 3164 (BSD) syslog lines, including files written by
358
+ rsyslog (no `<PRI>`, traditional or RFC 3339 timestamps), and CEF records, either bare or as the
359
+ message of a syslog line:
360
+
361
+ ```text
362
+ <34>1 2026-01-05T10:00:00Z web01 billing 4242 ID47 [auth@32473 email="alice.test@example.com"] login failed
363
+ Jan 5 10:00:00 web01 sshd[4242]: Accepted publickey for USER_TEST_001
364
+ <134>Jan 05 10:00:00 fw01 CEF:0|Vendor|Product|1.0|100|Login ok|5|suser=alice.test src=192.0.2.10
365
+ ```
366
+
367
+ It is never selected from the file extension.
368
+
369
+ - Every line is still scanned exactly like `text` (property-tested). Header fields become
370
+ `/timestamp`, `/hostname`, `/app_name`, `/procid`, `/msgid`; RFC 5424 structured data becomes
371
+ `/sd/<sd-id>/<name>`; the message is `/msg`.
372
+ - CEF header fields are `/cef/device_vendor`, `/cef/device_product`, `/cef/device_version`,
373
+ `/cef/signature_id`, `/cef/event_name` (CEF "Name") and `/cef/severity`; extensions are
374
+ `/cef/extension/<key>`. Common short keys also get a canonical name for the field-name
375
+ heuristics (`suser` is read as `source_user_name`, `src` as `source_ip`, `dhost` as
376
+ `destination_hostname`, ...), and `cs1`-style custom fields use their `cs1Label`. Findings keep
377
+ the raw key, so `field_names: [suser]` rules apply.
378
+ - Escapes are decoded for scanning and re-applied to replacements: `\"`, `\\`, `\]` in
379
+ structured data; `\|`, `\\` in CEF headers; `\=`, `\\`, `\n` in CEF extensions.
380
+ Header fields get `_` for whitespace (an RFC 3164 tag for anything outside
381
+ `[A-Za-z0-9_./-]`). Each rewritten line must still parse with all its fields, or the run fails.
382
+ - `-` (NILVALUE) fields get no field context. RFC 3164 lines need a `tag:` (or a CEF message) to
383
+ be recognised; other layouts, LEEF and multi-line messages get text-mode scanning only.
384
+ `drop_field` is not supported (it becomes `review`).
385
+
386
+ ### Stack traces (Python, JVM, Node.js)
387
+
388
+ `--format stacktrace` reads a log like `text` and also recognises stack trace lines, so a policy
389
+ can decide what leaves with them. It is never selected from the file extension.
390
+
391
+ - Every line is still scanned exactly like `text` (property-tested); a credential or email in a
392
+ frame or source line is found and handled whatever the policy does with the frame.
393
+ - Frame lines (Python `File "...", line N, in f`; JVM `at a.B.c(B.java:12)`; Node
394
+ `at fn (x.js:1:2)` and `at x.js:1:2`) and the indented Python source/caret lines under a frame
395
+ get a `proprietary.source` finding from the `stack_trace` detector, at `/stack/frames/N` and
396
+ `/stack/frames/N/source`. Rules can select them by category or by `detectors: [stack_trace]`.
397
+ A policy without such a rule falls back to `unknown_sensitive_behavior` (review or block).
398
+ Starter policies: `dev_local` keeps frames; `developer_logs` keeps them for `local_dev`,
399
+ `approved_internal` and `production_monitoring` and redacts them for every other destination;
400
+ `external_ai` redacts them; the other starter policies have no rule for them, so they fall
401
+ back to review (`prd_external_ai`: block).
402
+ - Other findings on trace lines get the trace path too: the Python exception line is
403
+ `/stack/exception`, JVM `Caused by:`/`Suppressed:` lines are `/stack/caused_by` and
404
+ `/stack/suppressed`.
405
+ - Redacting frames keeps the exception type and message for debugging:
406
+
407
+ ```yaml
408
+ - id: strip-stack-frames
409
+ priority: 200
410
+ match: {categories: [proprietary.source], destinations: [external_ai]}
411
+ action: redact
412
+ ```
413
+
414
+ - There is no lookahead and nothing is buffered: each line is classified from the lines before
415
+ it, keeps the `max_record_bytes` limit, and a trace of any length is streamed. So a JVM or Node
416
+ header line (`Exception in thread ...`, `TypeError: ...`) is not marked as part of the trace,
417
+ and frames are recognised on their own (a truncated or interleaved trace still has every frame
418
+ flagged). An unrelated log line ends a trace; frame numbering restarts after it.
419
+ - Matching is structural, not semantic: any indented `at name (...)` line counts as a frame, and
420
+ Python source lines are only recognised after a traceback header or frame. Python exception
421
+ groups (`| ` prefixes), .NET, Go, Ruby and other runtimes get text-mode scanning only.
422
+ `drop_field` is not supported (it becomes `review`).
423
+
424
+ ### Pipes (stdin/stdout)
425
+
426
+ Use `-` as INPUT to read stdin (then `--format` is required), and `sanitize --output -` to write
427
+ the sanitized data to stdout:
428
+
429
+ ```bash
430
+ kubectl logs my-pod | ciphergate sanitize - --format text --policy policies/prd_external_ai.yml \
431
+ --environment prd --target-environment external --destination external_ai \
432
+ --output - --apply --report build/report.json > build/safe.log
433
+ ```
434
+
435
+ - stdout receives data **only after every check passed**. A blocked, review, rescan-failed or
436
+ errored run writes nothing to stdout and exits nonzero, so always check the exit status (in
437
+ shells, `set -o pipefail`).
438
+ - With `--output -` the human summary goes to stderr. Use `--report PATH` for the JSON report;
439
+ `--report -` and `--report-format json` without `--report` are refused because stdout carries
440
+ the data.
441
+ - stdin is copied into a private temporary workspace (owner-only on POSIX), capped by
442
+ `--max-bytes`, and deleted when the run ends. Reports name the input `<stdin>` and the output
443
+ `<stdout>`.
444
+ - If the reader closes the pipe early, the run exits 5 (`stdout_broken_pipe`): what it received
445
+ is incomplete.
446
+ - A file actually named `-` must be passed as `./-`.
447
+
448
+ | Code | Meaning |
449
+ |---|---|
450
+ | 0 | Completed; no block/review; required context validated |
451
+ | 1 | Blocked, review required, missing/conflicting context |
452
+ | 2 | Command-line usage error |
453
+ | 3 | Policy syntax/schema/semantic error |
454
+ | 4 | Input parse/encoding/size/format error |
455
+ | 5 | Output/path error (exists without `--force`, alias, symlink, write failure) |
456
+ | 6 | Key or transform configuration missing/invalid, or a value a transform cannot handle |
457
+ | 7 | Strict rescan found gating residual findings (output withheld) |
458
+ | 8 | Unexpected internal error (safe message) |
459
+ | 130 | Interrupted (no output published) |
460
+
461
+ ### Troubleshooting
462
+
463
+ - `destination_required_but_missing` — pass `--destination` (and `--environment`).
464
+ - `environment_context_conflict` — the policy pins that value; use a matching policy.
465
+ - `no_applicable_rule_requires_review` — add a rule for that category; inspect lists them.
466
+ - `key_missing` / exit 6 — set `CIPHERGATE_HMAC_KEY` (32+ bytes) or `CIPHERGATE_HMAC_KEY_FILE`.
467
+ - `key_file_invalid` / exit 6 — the key file is unreadable, too large, not a regular file,
468
+ accessible to group/others (POSIX), or both the variable and `_FILE` are set.
469
+ - `format_required` — unknown extension; pass `--format`.
470
+
471
+ ## Python API
472
+
473
+ ```python
474
+ from ciphergate import load_policy, sanitize_file, ContextInput, Environment, Destination
475
+ policy = load_policy("policies/developer_logs.yml")
476
+ report = sanitize_file("app.log", "app.clean.log", policy,
477
+ ContextInput(environment=Environment.DEV, destination=Destination.LOCAL_DEV))
478
+ print(report.status, report.exit_code)
479
+ ```
480
+
481
+ Streams and directories use the same pipeline and the same safety rules as the CLI:
482
+
483
+ ```python
484
+ import io
485
+ from ciphergate import InputFormat, SanitizeOptions, sanitize_stream, sanitize_directory
486
+
487
+ out = io.BytesIO()
488
+ report = sanitize_stream(open("app.log", "rb"), out, policy, ctx,
489
+ SanitizeOptions(format=InputFormat.TEXT))
490
+ # out is written only if report.ok; a blocked/review/failed run leaves it untouched.
491
+
492
+ batch = sanitize_directory("logs/", "logs.clean/", policy, ctx, recursive=True)
493
+ print(batch.status, batch.exit_code, batch.to_dict()["summary"])
494
+ ```
495
+
496
+ - `*_stream` functions take binary streams (`"rb"`, `io.BytesIO`) and need an explicit format
497
+ (`fmt=` or `SanitizeOptions.format`). The input is spooled to a private temporary workspace,
498
+ capped at `max_input_bytes`, and removed afterwards. `sanitize_stream` copies to `output`
499
+ only after every check has passed. If that copy fails part-way, it raises `OutputWriteError`
500
+ (`stream_write_failed`), and `output` may hold an incomplete prefix of sanitized data.
501
+ - `*_directory` functions return a `BatchReport` (`status`, `exit_code`, `files`, `skipped`,
502
+ `to_dict()` in the `ciphergate.batch-report/v1` schema). `sanitize_directory` needs a new
503
+ output directory outside the input, publishes it only if every file was sanitized, and refuses
504
+ `SanitizeOptions(force=True)`.
505
+ - As with `sanitize_file`, `SanitizeOptions()` defaults to `apply=True`; pass `apply=False` for
506
+ a plan. Keys come from the environment unless `SanitizeOptions(keys=...)` is given.
507
+
508
+ Supported API: `load_policy`, `validate_policy`, `inspect_file`, `plan_file`, `sanitize_file`,
509
+ `rescan_file`, the `*_stream` and `*_directory` functions above, and the types exported from
510
+ `ciphergate` (including `BatchReport`). Other modules are internal.
511
+
512
+ ## Documentation
513
+
514
+ [Architecture](docs/architecture.md) · [Policy schema](docs/policy-schema.md) ·
515
+ [Detector coverage](docs/detector-coverage.md) · [Threat model](docs/threat-model.md) ·
516
+ [Security & privacy](docs/security-and-privacy.md) · [Limitations](docs/limitations.md) ·
517
+ [Examples](docs/examples.md) · [Claude Code tooling](docs/claude-code-tooling.md) ·
518
+ [Security policy](SECURITY.md) · [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md)
519
+
520
+ ## Development
521
+
522
+ ```bash
523
+ python -m pytest -q --cov=ciphergate
524
+ ruff check src tests scripts && ruff format --check src tests scripts
525
+ mypy
526
+ ```
527
+
528
+ ## License
529
+
530
+ Licensed under the [Apache License, Version 2.0](LICENSE); see also [NOTICE](NOTICE). The license
531
+ covers the software only: it is not legal advice and makes no compliance claim.
@@ -0,0 +1,74 @@
1
+ ciphergate/__init__.py,sha256=1xN3LaV2hJv7xNAPlM_cOCF_8PvSCDcKK5JBt3z8aYg,2844
2
+ ciphergate/__main__.py,sha256=wOnYViq_FCF3tHyKiX6J3YVjqOZv67Fq87IMpYhAuJI,110
3
+ ciphergate/cli.py,sha256=vymgmj7rIKFqaDdpgkY4uhIPHadfem8g7wg5pdos0zM,28502
4
+ ciphergate/config.py,sha256=m7MIFFue-cg9EIbUNw6oJNO5xTT-YkcMh8SSBRXplL0,1764
5
+ ciphergate/constants.py,sha256=pWnNutyJ622cYkNFkwNpzm6-gaMcAkD5ocR4-T9Rj3k,8360
6
+ ciphergate/errors.py,sha256=JFO5Ungzd_l7QlfHUs13AmeFBkYgCyEb9J6-Qur_ZzI,3315
7
+ ciphergate/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ ciphergate/detectors/__init__.py,sha256=OxjG9zduL0wA7xVl8XWxtFJcUyy_r3oblhIR1U2p8YI,219
9
+ ciphergate/detectors/base.py,sha256=Uwr3QoapU7vPcigjqC415oUTEjpNIURM7VfXl76iJSU,1672
10
+ ciphergate/detectors/custom_regex.py,sha256=yyWgZoDdz2BBOuScBXsTCN789LNsdgQYmr34EodAk6Y,1279
11
+ ciphergate/detectors/dates.py,sha256=VV2Yp2LPAKzeQeHj-1JHuJwBKI5ktBOar5uKMc5Z1R8,3441
12
+ ciphergate/detectors/email.py,sha256=mjPJEq_GwKiSFy9G9TlPuJpWngM995LHunlTfsd4K1M,1848
13
+ ciphergate/detectors/field_names.py,sha256=3sC1OiI2-RW9N9Pi_v1wdjD7KSCHrGpnpIOik7AritQ,7446
14
+ ciphergate/detectors/ip_address.py,sha256=o_Wg0C2HqU_wu6HumNv5sfzKWdNrTgZt8FRuWH03Ggc,2976
15
+ ciphergate/detectors/network.py,sha256=FuCOXKSjg0qx8vPlg0PHR4yePDWGiOgB5PDYC_XmSik,1385
16
+ ciphergate/detectors/payment_card.py,sha256=UUy2QwzeCtCINB9U6sI1ABp3Q0f43i1t0iZ1Vq96LMQ,2622
17
+ ciphergate/detectors/phone.py,sha256=1SZIM024gNk8jK_D6ci3drajvN75vtlWEs6aJ_gLK0E,2628
18
+ ciphergate/detectors/presidio_ner.py,sha256=Fz9I5m3u6nFX1NqbRyRNWFo0XBl7t0NyKOgaTIaWgr4,4745
19
+ ciphergate/detectors/regex_safety.py,sha256=ICxCd77SEvpmDUzzPgc6qOzYlyqSrQ5lLsrP_2ZGs1w,1655
20
+ ciphergate/detectors/registry.py,sha256=qt-IDZ1_gUYK0cyT0YV-UpxcvNEfjV68mdukcWhKRT8,7966
21
+ ciphergate/detectors/secrets.py,sha256=atg5jKqpIVouWYZbM_wMdt1Wqsl584O2V3wQk6o6N-o,4808
22
+ ciphergate/models/__init__.py,sha256=kS94nQBQL7fsR9rXgQPTHoFMeMWSCfR-Wv-U1o7npWM,68
23
+ ciphergate/models/context.py,sha256=X-2118oXDYSjNiF_K_-BmjKqATVUMwLVQvW4r-sxALE,2227
24
+ ciphergate/models/finding.py,sha256=co5VSbZ3qzYXCjP4XcrrCEBeStaP0nzhuXIDLNLYAHY,3103
25
+ ciphergate/models/policy.py,sha256=GmsNG9WZdacnZoD2sAUMKFbeZW0W4eQB-_FmT_vwNqA,14637
26
+ ciphergate/models/report.py,sha256=71aakSmUalBHJHkA_BsmEucHojgN4ZVUMXOw-WQdn4Y,3737
27
+ ciphergate/pipeline/__init__.py,sha256=5Q5K19ubA2aCwyux5zVZM002PQJ841kkJkpRl-0UPZM,84
28
+ ciphergate/pipeline/atomic_output.py,sha256=AHBhsbRA9RRsPoxTCJ1V12a6FUnzh4RpGXovUtLGwKs,5670
29
+ ciphergate/pipeline/batch.py,sha256=oIb7Wt0JXARN7uqCIPZ1xg-C8PH9KPqQ_7Adz4oWqss,19348
30
+ ciphergate/pipeline/inspector.py,sha256=mho9CvuEBsAG7fBjir7NfW2tINoDYAsTxU_FSDo-SLA,12185
31
+ ciphergate/pipeline/paths.py,sha256=z1p8LlMTFrAhVDHqkpFuP2WIJHIgjfq5qHXDVafuZZU,3523
32
+ ciphergate/pipeline/planner.py,sha256=QjhoP-v4PzwD2qopTT4NF7QTYi1vmp4fLDAQtjJn0GM,7680
33
+ ciphergate/pipeline/rescan.py,sha256=eTYITPkDdWBNiHOolLWs-k7XYid-LyQMxISEL_M3cDE,5438
34
+ ciphergate/pipeline/sanitizer.py,sha256=ThmTLAlqNbfPPlKMz4snkpsKQqN4HGPotjKRaVZmEsw,14884
35
+ ciphergate/pipeline/stdio.py,sha256=NnBihUn6zr2v1UFBji9BuSjF6ijnn0M_CIyqolJCo_I,6376
36
+ ciphergate/pipeline/streams.py,sha256=axAz5jeKhgO7hrO90hqMtXg6JVy2mcjuitjyHzwQxz4,5635
37
+ ciphergate/policy/__init__.py,sha256=uUbLirNpTweVcnTZoSV_VRgR3yI4BkX7GJiBVFAtmIs,617
38
+ ciphergate/policy/context.py,sha256=8tHgqc8eWRhD96JP2iDIeS1GSgbBjhKw2U7KYKHTmG8,7370
39
+ ciphergate/policy/engine.py,sha256=OuYXyyxbBAQFVY64whRJmP3x7F7hjWjIWp6jXcL123w,7657
40
+ ciphergate/policy/loader.py,sha256=FQh5L6uv1HxaKC-1f9wAk2r-3H6gmex7ipvkaLS_X6I,4555
41
+ ciphergate/policy/precedence.py,sha256=jGs5gXGcxSvyovHerGStdDZCqTZw4TT3xbDT6G2BwGI,5272
42
+ ciphergate/policy/validator.py,sha256=DpoReXH6wFc1B9LXJf4snLe8Kz-V9X73m2S9IDhB91A,25743
43
+ ciphergate/readers/__init__.py,sha256=JfcbPlaO7qUZOde9VPLdMyW8BV8luNZfQ6XCRn_iD6c,3981
44
+ ciphergate/readers/accesslog_reader.py,sha256=j8Ku5pdcu5Rs6nIjAFw78jdcg-MEA8Od-uaVGpF4LUE,9701
45
+ ciphergate/readers/base.py,sha256=mew-8ISMrwKgsbaiuS4odQqaMwVB5aD7hZ2i1l-qnPg,3942
46
+ ciphergate/readers/csv_reader.py,sha256=0xvwv1TbmZAfN0oZYje1OVezWQnRl6bm8YakUmN8Alw,9297
47
+ ciphergate/readers/json_reader.py,sha256=_f-mxC-KySygwv_uI4t2zIBDyb6CB8qkYyd1-VtK8ME,6190
48
+ ciphergate/readers/jsonl_reader.py,sha256=LPmOPKh0T04FZSc4ER3_QBdz80lvlwQmvlkyyAWycAU,3530
49
+ ciphergate/readers/jsonlog_reader.py,sha256=fxqN02VjHT6Y8nu5xksImPgF7CcYmJB1pDoA_4YJCN4,6442
50
+ ciphergate/readers/line_structure.py,sha256=ghTcx1hS6EhvdKVTXJSfme09kw4n_Lzyt7-M0HRs8cU,5411
51
+ ciphergate/readers/logfmt_reader.py,sha256=qvU768yslj_HnX5xvTew06q-12XzlPSCq7AtXMwLCGY,3693
52
+ ciphergate/readers/stacktrace_reader.py,sha256=ZPE_761YoqQaeGpJH8nAQD0RkgWyRywapr1-awOS6Bk,4383
53
+ ciphergate/readers/syslog_reader.py,sha256=3Jry_YxE6w0SxwHAZfgtrFVvcYAmsThr0hXjeXktM4s,11845
54
+ ciphergate/readers/text_reader.py,sha256=1Vc1MRljomHa5cfVPHZ0ot34VBDftS7y_bfI8EFncMI,1991
55
+ ciphergate/reporting/__init__.py,sha256=YP7P1DweiCe0pzmrlzhdS413k0Heuz4_ND5l-32AdpA,74
56
+ ciphergate/reporting/audit.py,sha256=saIsbTzjgE7kNS7jG2RoBCCzCGDk71JTF1fXSCMW6EU,2895
57
+ ciphergate/reporting/safe_text.py,sha256=TN8BkILdnN3N6DaevjKKALbGUlvX6V8ajBxsOwRU0rI,2417
58
+ ciphergate/reporting/serializer.py,sha256=Dliu2QXHYpYFvxZer6FuHpExWqgqs_CNZ-E0VsIlLYo,1443
59
+ ciphergate/reporting/terminal.py,sha256=C05-5kGFXiXKEncW-pcVlImVe4KUhmAOY7yFu-m3J9I,6226
60
+ ciphergate/templates/starter_policy.yml,sha256=VC-9ZvqgWq8U2SKTDT3sielVM4h2rcRHaTUu11rD06Y,2448
61
+ ciphergate/transforms/__init__.py,sha256=-WjKEtECFJkDFkW98AGhFCqaR16eIxvr8qoK_OjjnZ8,265
62
+ ciphergate/transforms/date_shift.py,sha256=se3RXh6aoe-diztdA9g4SvfcEKzr0865T9setM6oORs,3181
63
+ ciphergate/transforms/generalize.py,sha256=v6KPKrSEfqIc9GDFySfuY_PJOIdci-NRN_qoAxGryF8,3824
64
+ ciphergate/transforms/keys.py,sha256=suHEFv-sTHFD2fy5UcBVVgoYnwjei_X8dsXCNsv56nE,6697
65
+ ciphergate/transforms/pseudonymize.py,sha256=idn948Jw89jgZKH_q0JqRGX8ADJ4lRjALzo9GieKBRQ,4008
66
+ ciphergate/transforms/redact.py,sha256=Zq4cXuO5SvUq6vPTBBJTFlkJ9kPArOmy6tt7vAuftDk,297
67
+ ciphergate/transforms/registry.py,sha256=xL8mxaIkJDg_jmAnDqOmI0NSljrIjQ2tIPDRzLoYEt8,7902
68
+ ciphergate/transforms/replace.py,sha256=kzN5p1vTA36sNn9dqxXTwgmE7H1kc7AAO8wGHijZFWQ,590
69
+ ciphergate_cli-0.7.0.dist-info/METADATA,sha256=7AxESHLmdAsIlHgg_dO1GEaSTgDyFB8SpQswSoRu8wk,29268
70
+ ciphergate_cli-0.7.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
71
+ ciphergate_cli-0.7.0.dist-info/entry_points.txt,sha256=-VbFZjjWHKqw4YXsBtZh1bkvyFcAfNZiKIeALxFSFwo,51
72
+ ciphergate_cli-0.7.0.dist-info/licenses/LICENSE,sha256=yVuuHRzgI17MzTVgt3LsHvuX80innw--CmNPDCzO_iw,11358
73
+ ciphergate_cli-0.7.0.dist-info/licenses/NOTICE,sha256=UuXmeAv3VIfY3JgnqbyTz_9DbNL4w60eSgIEpQGE2bo,119
74
+ ciphergate_cli-0.7.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ciphergate = ciphergate.cli:main