ciphergate-cli 0.15.0__tar.gz → 0.17.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.
Files changed (162) hide show
  1. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/CHANGELOG.md +61 -1
  2. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/PKG-INFO +77 -5
  3. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/README.md +76 -4
  4. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/docs/architecture.md +2 -0
  5. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/docs/detector-coverage.md +25 -2
  6. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/docs/limitations.md +17 -0
  7. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/docs/policy-schema.md +72 -0
  8. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/docs/security-and-privacy.md +15 -0
  9. ciphergate_cli-0.17.0/policies/hospital_us_override.yml +46 -0
  10. ciphergate_cli-0.17.0/policies/packs/eu_gdpr.yml +78 -0
  11. ciphergate_cli-0.17.0/policies/packs/healthcare_baseline.yml +109 -0
  12. ciphergate_cli-0.17.0/policies/packs/india_dpdp.yml +97 -0
  13. ciphergate_cli-0.17.0/policies/packs/us_hipaa.yml +80 -0
  14. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/scripts/benchmark/README.md +8 -0
  15. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/__init__.py +1 -1
  16. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/cli.py +85 -0
  17. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/models/policy.py +5 -0
  18. ciphergate_cli-0.17.0/src/ciphergate/pipeline/review.py +528 -0
  19. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/pipeline/sanitizer.py +14 -1
  20. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/policy/engine.py +5 -0
  21. ciphergate_cli-0.17.0/src/ciphergate/policy/layers.py +508 -0
  22. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/policy/loader.py +11 -0
  23. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/policy/templates.py +1 -12
  24. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/policy/validator.py +33 -2
  25. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/reporting/audit.py +14 -0
  26. ciphergate_cli-0.17.0/src/ciphergate/reporting/review_page.py +871 -0
  27. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/reporting/safe_text.py +10 -3
  28. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/reporting/terminal.py +2 -0
  29. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/transforms/registry.py +5 -3
  30. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/integration/test_cli_e2e.py +28 -1
  31. ciphergate_cli-0.17.0/tests/integration/test_review.py +255 -0
  32. ciphergate_cli-0.17.0/tests/security/test_review_page.py +48 -0
  33. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/tooling/test_benchmark.py +55 -0
  34. ciphergate_cli-0.17.0/tests/unit/test_policy_layers.py +438 -0
  35. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/.gitignore +0 -0
  36. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/LICENSE +0 -0
  37. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/NOTICE +0 -0
  38. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/SECURITY.md +0 -0
  39. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/docs/claude-code-tooling.md +0 -0
  40. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/docs/examples.md +0 -0
  41. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/docs/threat-model.md +0 -0
  42. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/examples/expected/sample.dev_local_dev.csv +0 -0
  43. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/examples/expected/sample.dev_local_dev.json +0 -0
  44. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/examples/expected/sample.dev_local_dev.jsonl +0 -0
  45. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/examples/expected/sample.dev_local_dev.log +0 -0
  46. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/examples/sample.csv +0 -0
  47. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/examples/sample.json +0 -0
  48. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/examples/sample.jsonl +0 -0
  49. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/examples/sample.log +0 -0
  50. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/policies/dev_local.yml +0 -0
  51. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/policies/developer_logs.yml +0 -0
  52. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/policies/external_ai.yml +0 -0
  53. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/policies/financial_template.yml +0 -0
  54. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/policies/healthcare_template.yml +0 -0
  55. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/policies/local_debug.yml +0 -0
  56. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/policies/multi_environment.yml +0 -0
  57. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/policies/prd_external_ai.yml +0 -0
  58. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/policies/stg_test.yml +0 -0
  59. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/policies/test_dataset.yml +0 -0
  60. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/pyproject.toml +0 -0
  61. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/__main__.py +0 -0
  62. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/config.py +0 -0
  63. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/constants.py +0 -0
  64. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/__init__.py +0 -0
  65. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/base.py +0 -0
  66. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/custom_regex.py +0 -0
  67. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/dates.py +0 -0
  68. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/email.py +0 -0
  69. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/field_names.py +0 -0
  70. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/iban.py +0 -0
  71. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/ip_address.py +0 -0
  72. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/mac_address.py +0 -0
  73. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/network.py +0 -0
  74. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/payment_card.py +0 -0
  75. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/phone.py +0 -0
  76. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/presidio_ner.py +0 -0
  77. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/regex_safety.py +0 -0
  78. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/registry.py +0 -0
  79. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/secrets.py +0 -0
  80. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/detectors/ssn.py +0 -0
  81. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/errors.py +0 -0
  82. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/models/__init__.py +0 -0
  83. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/models/context.py +0 -0
  84. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/models/finding.py +0 -0
  85. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/models/report.py +0 -0
  86. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/pipeline/__init__.py +0 -0
  87. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/pipeline/atomic_output.py +0 -0
  88. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/pipeline/batch.py +0 -0
  89. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/pipeline/inspector.py +0 -0
  90. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/pipeline/paths.py +0 -0
  91. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/pipeline/planner.py +0 -0
  92. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/pipeline/rescan.py +0 -0
  93. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/pipeline/stdio.py +0 -0
  94. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/pipeline/streams.py +0 -0
  95. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/policy/__init__.py +0 -0
  96. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/policy/context.py +0 -0
  97. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/policy/precedence.py +0 -0
  98. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/py.typed +0 -0
  99. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/__init__.py +0 -0
  100. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/accesslog_reader.py +0 -0
  101. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/base.py +0 -0
  102. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/csv_reader.py +0 -0
  103. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/json_reader.py +0 -0
  104. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/jsonl_reader.py +0 -0
  105. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/jsonlog_reader.py +0 -0
  106. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/line_structure.py +0 -0
  107. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/logfmt_reader.py +0 -0
  108. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/stacktrace_reader.py +0 -0
  109. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/syslog_reader.py +0 -0
  110. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/readers/text_reader.py +0 -0
  111. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/reporting/__init__.py +0 -0
  112. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/reporting/next_steps.py +0 -0
  113. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/reporting/serializer.py +0 -0
  114. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/templates/starter_policy.yml +0 -0
  115. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/transforms/__init__.py +0 -0
  116. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/transforms/date_shift.py +0 -0
  117. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/transforms/generalize.py +0 -0
  118. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/transforms/keys.py +0 -0
  119. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/transforms/pseudonymize.py +0 -0
  120. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/transforms/redact.py +0 -0
  121. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/src/ciphergate/transforms/replace.py +0 -0
  122. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/__init__.py +0 -0
  123. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/conftest.py +0 -0
  124. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/integration/__init__.py +0 -0
  125. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/integration/test_api_batch_streams.py +0 -0
  126. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/integration/test_atomic_output.py +0 -0
  127. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/integration/test_batch.py +0 -0
  128. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/integration/test_domain_templates.py +0 -0
  129. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/integration/test_environment_matrix.py +0 -0
  130. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/integration/test_pipelines.py +0 -0
  131. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/integration/test_presidio_real.py +0 -0
  132. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/integration/test_stdio.py +0 -0
  133. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/regression/__init__.py +0 -0
  134. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/regression/test_known_cases.py +0 -0
  135. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/security/__init__.py +0 -0
  136. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/security/test_key_file.py +0 -0
  137. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/security/test_no_network.py +0 -0
  138. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/security/test_no_raw_value_leaks.py +0 -0
  139. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/security/test_path_safety.py +0 -0
  140. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/security/test_policy_fail_closed.py +0 -0
  141. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/security/test_review_findings.py +0 -0
  142. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/tooling/__init__.py +0 -0
  143. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/tooling/test_ci_supply_chain.py +0 -0
  144. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/tooling/test_claude_tooling.py +0 -0
  145. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/tooling/test_git_tooling.py +0 -0
  146. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/tooling/test_publish_workflow.py +0 -0
  147. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/__init__.py +0 -0
  148. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_accesslog.py +0 -0
  149. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_detectors.py +0 -0
  150. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_jsonlog.py +0 -0
  151. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_logfmt.py +0 -0
  152. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_next_steps.py +0 -0
  153. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_policy_engine.py +0 -0
  154. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_policy_precedence.py +0 -0
  155. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_policy_templates.py +0 -0
  156. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_policy_validation.py +0 -0
  157. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_presidio_ner.py +0 -0
  158. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_properties.py +0 -0
  159. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_readers.py +0 -0
  160. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_stacktrace.py +0 -0
  161. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_syslog.py +0 -0
  162. {ciphergate_cli-0.15.0 → ciphergate_cli-0.17.0}/tests/unit/test_transforms.py +0 -0
@@ -6,6 +6,64 @@ versioning: [PEP 440](https://peps.python.org/pep-0440/) with SemVer meaning (pr
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.17.0] - 2026-11-02
10
+
11
+ Policies can now be built in layers. A shipped baseline pack, a jurisdiction pack (US, EU or
12
+ India) and your own small file merge into one policy, and a later layer cannot quietly weaken
13
+ an earlier one. The packs are templates requiring review, not legal or compliance advice.
14
+
15
+ ### Added
16
+
17
+ - Policy layering: `extends: [pack, ...]` builds one effective policy from shipped packs plus
18
+ your own file. Layers merge so that a later layer can never quietly weaken an earlier one.
19
+ Conflicting context pins, weaker settings or requirements, disabled detectors, and rules or
20
+ defaults that would override an earlier block, review or transform with something weaker
21
+ fail validation. A rule may weaken an earlier rule or default only by naming it in the new
22
+ `relaxes` field; this is reported as a warning. Earlier `hard` and `guardrail` rules can
23
+ never be relaxed.
24
+ - Shipped packs (templates requiring review): `healthcare_baseline`, a strict global healthcare
25
+ default, and `us_hipaa`, field classifications modelled on the HIPAA Safe Harbor identifier
26
+ list, with ages over 89 shown as `90+`. `us_hipaa` is not Safe Harbor or Expert Determination
27
+ by itself.
28
+ - New template `hospital_us_override`, an example hospital layer on top of `us_hipaa`.
29
+ - Packs `eu_gdpr` and `india_dpdp` (templates requiring review, not legal advice). `eu_gdpr`
30
+ blocks GDPR Art. 9 special-category fields toward external destinations and reviews them
31
+ elsewhere, and finds member-state identifier fields plus Italian codice fiscale values.
32
+ `india_dpdp` finds grouped Aadhaar numbers, ABHA numbers and addresses, PAN and Indian
33
+ identifier fields, and blocks SPDI-style sensitive fields toward external destinations. One
34
+ policy can extend several jurisdiction packs.
35
+
36
+ ### Changed
37
+
38
+ - When equally protective findings overlap in one value, the more confident one now names the
39
+ redaction marker (for example a grouped Aadhaar number is marked `pii.government_id`, not
40
+ `pii.phone`). What gets removed is unchanged.
41
+ - Reports list the policy's layers (name, kind, source, file digest), and each decision names
42
+ the layer that made it. `policy.digest` now covers every layer. `validate-policy` prints the
43
+ layer chain and adds `layers` to its JSON output. The terminal summary and the review page
44
+ show the layer next to the rule id.
45
+
46
+ ## [0.16.0] - 2026-11-02
47
+
48
+ `ciphergate review` writes a local HTML page that shows, as a trace, what sanitize would change
49
+ and how the run went. The CLI's other commands and the report format are unchanged.
50
+
51
+ ### Added
52
+
53
+ - `ciphergate review INPUT --policy P -o page.html` writes one local HTML page that shows
54
+ what sanitize would change. It covers findings, how many are transformed, kept, blocked
55
+ or waiting for review, a per-category table, rescan results, possible partial finds,
56
+ and each record with its changes highlighted and labelled with category, action, rule
57
+ and reason code. No sanitized file is written. The page holds the would-be output; the
58
+ raw input is added only with `--include-input`. The page has no scripts, loads nothing,
59
+ is written atomically, and is not overwritten without `--force`. The exit code matches
60
+ `plan`. The page is laid out as a trace: a tree of the run, its steps with timings, and the
61
+ records; each record opens as Input and Output panels.
62
+ - `scripts/benchmark/ner_names.py` measures the optional NER detector on generated log lines.
63
+ With `en_core_web_sm` it found any part of 57-59% of names and the whole name in 41%, missed
64
+ most lowercase and upper-case names, and flagged 3 of 16 name-free lines. Details and limits
65
+ are in `docs/detector-coverage.md`. The detector itself is unchanged.
66
+
9
67
  ## [0.15.0] - 2026-11-02
10
68
 
11
69
  JSON reports now include the next-step hints, so CI pipelines can act on them. The field is new;
@@ -395,7 +453,9 @@ Pre-release (alpha). Licensed under Apache-2.0. Not a final release: the securit
395
453
  See `docs/limitations.md`. Deferred: optional NER/Presidio detectors, stdin/stdout streaming,
396
454
  directory processing, structured log parsers, key files, reversible token vault, lossy decoding.
397
455
 
398
- [Unreleased]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.15.0...HEAD
456
+ [Unreleased]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.17.0...HEAD
457
+ [0.17.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.16.0...v0.17.0
458
+ [0.16.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.15.0...v0.16.0
399
459
  [0.15.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.14.0...v0.15.0
400
460
  [0.14.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.13.0...v0.14.0
401
461
  [0.13.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.12.0...v0.13.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ciphergate-cli
3
- Version: 0.15.0
3
+ Version: 0.17.0
4
4
  Summary: Local-first, environment- and destination-aware inspection and sanitization of logs and test datasets.
5
5
  Project-URL: Homepage, https://github.com/aashishkumar-tech/ciphergate
6
6
  Project-URL: Source, https://github.com/aashishkumar-tech/ciphergate
@@ -50,7 +50,7 @@ vendor ticket. CipherGate replaces that with a local, explainable, scriptable ga
50
50
  inspect -> plan -> sanitize --apply -> rescan
51
51
  ```
52
52
 
53
- > **Status: 0.15.0 (alpha).** No detector finds every sensitive value. A passing run means the
53
+ > **Status: 0.17.0 (alpha).** No detector finds every sensitive value. A passing run means the
54
54
  > configured processing completed under the selected policy; it is **not** proof that the output
55
55
  > contains no sensitive information, and it is not a compliance (GDPR, HIPAA, PCI DSS, ...) or
56
56
  > anonymization guarantee. Policy templates must be reviewed before use.
@@ -91,6 +91,8 @@ inspect -> plan -> sanitize --apply -> rescan
91
91
  and its source, gates, key *status* (never key material), no raw values or fingerprints.
92
92
  A failed run's report lists `next_steps` as `{"code", "message"}` pairs, the same hints the
93
93
  terminal prints as `next:` lines, so CI can act on them.
94
+ - **Review page:** `ciphergate review` writes one local HTML file that shows what sanitize
95
+ would change, record by record, with run metrics. See [Review page](#review-page).
94
96
 
95
97
  ## Install
96
98
 
@@ -230,14 +232,40 @@ controlled pipeline configuration.
230
232
  ## Shipped policy templates (review before use)
231
233
 
232
234
  `dev_local`, `stg_test`, `prd_external_ai`, `developer_logs`, `test_dataset`, `external_ai`,
233
- `healthcare_template`, `financial_template`, `local_debug`, and `multi_environment` (one policy
235
+ `healthcare_template`, `financial_template`, `local_debug`, `hospital_us_override` (a layered example, see below), and `multi_environment` (one policy
234
236
  whose outcomes vary by environment *and* destination). All validate in CI. Templates named for
235
237
  healthcare or finance are starting points, **not** HIPAA/PCI controls.
236
238
 
239
+ ### Policy layering
240
+
241
+ A policy can `extends:` shipped packs, so a team keeps only its own differences in a small file:
242
+
243
+ ```text
244
+ healthcare_baseline (global strict default) -> us_hipaa (US identifier list) -> your file
245
+ ```
246
+
247
+ ```bash
248
+ ciphergate init --template hospital_us_override --output my-hospital.yml
249
+ ciphergate validate-policy my-hospital.yml
250
+ # layers: healthcare_baseline -> us_hipaa -> hospital-us-override
251
+ ```
252
+
253
+ A later layer can add context, detectors, classifications and stricter rules. It cannot quietly
254
+ weaken an earlier one: a weaker setting, a disabled detector or a rule that would override a
255
+ block, review or redaction with something weaker fails validation unless the rule names what it
256
+ relaxes (`relaxes: [rule-id]`), which is then reported as a warning. Hard and guardrail rules
257
+ can never be relaxed. Reports list every layer with its digest and show which layer decided
258
+ each value. Jurisdiction packs: `us_hipaa` (modelled on the HIPAA Safe Harbor identifier list
259
+ but **not** Safe Harbor or Expert Determination by itself), `eu_gdpr` (GDPR special categories
260
+ and member-state identifiers) and `india_dpdp` (Aadhaar, ABHA, PAN and sensitive fields). None
261
+ of them is legal advice or makes processing lawful. A policy can extend several. See
262
+ [docs/policy-schema.md](docs/policy-schema.md#layering-extends).
263
+
237
264
  ## Commands and exit codes
238
265
 
239
- `init`, `validate-policy`, `inspect`, `plan`, `sanitize` (needs `--apply`), `rescan`, `explain`
240
- (decision for a hypothetical finding — no data needed). All support `--help`.
266
+ `init`, `validate-policy`, `inspect`, `plan`, `sanitize` (needs `--apply`), `rescan`, `review`
267
+ (local HTML page, see below), `explain` (decision for a hypothetical finding — no data
268
+ needed). All support `--help`.
241
269
 
242
270
  Common options: `--policy`, `--format csv|json|jsonl|text|logfmt|jsonlog|accesslog|syslog|stacktrace`, `--report PATH`,
243
271
  `--report-format json|text` (JSON without `--report` prints only JSON on stdout),
@@ -485,6 +513,50 @@ kubectl logs my-pod | ciphergate sanitize - --format text --policy policies/prd_
485
513
  | 8 | Unexpected internal error (safe message) |
486
514
  | 130 | Interrupted (no output published) |
487
515
 
516
+ ### Review page
517
+
518
+ `ciphergate review` runs the same steps as `sanitize --apply`, including the rescan, but writes
519
+ no sanitized file. It writes one HTML page instead:
520
+
521
+ ```bash
522
+ ciphergate review app.log --policy policies/developer_logs.yml --environment dev --destination local_dev -o build/review.html
523
+ ```
524
+
525
+ Open the file in a browser. It is laid out like a trace: a tree on the left, details of the
526
+ selected item on the right.
527
+
528
+ - **review run:** a verdict, then metrics. These include findings and how many are
529
+ transformed, kept by an `allow` rule, need review or are blocked (with percentages), records
530
+ with findings, values changed, records dropped, rescan results and possible partial finds.
531
+ - **Steps**, with measured timings: the plan pass (read input, detect, decide), transform,
532
+ rescan and output.
533
+ - *detect* shows counts by category, detector and confidence.
534
+ - *decide* shows the policy and context, what happened to each category, counts by action,
535
+ rule and reason code, plus gates and key status.
536
+ - **Records with findings:** each one opens as Input and Output panels with a decisions table.
537
+ Changed parts are highlighted, and hovering over one shows its category, action, rule and
538
+ reason code. Values kept by an `allow` rule are underlined. A dashed outline marks a
539
+ *possible partial find*: a replaced name or address next to a capitalised word that stayed.
540
+ This is a hint, not a check.
541
+
542
+ What the page contains, and how to handle it:
543
+
544
+ - By default it contains the would-be **output**, not the input. Treat it like the sanitized
545
+ file: detectors can miss values.
546
+ - `--include-input` also adds the **raw input** next to the output, with a warning banner. A
547
+ page made with it must be handled like the original file. Do not share, upload or commit it.
548
+ - If the run is blocked, needs review or a key is missing, there is no output preview. The page
549
+ shows the plan and the decisions for each record. Planned transforms are labelled as not run.
550
+ - There are no default locations: `-o` is required. `build/` is in `.gitignore`. The page is not
551
+ overwritten without `--force`, can't be the input or the policy file, and is written
552
+ atomically (0600 on POSIX). Directories and stdin/stdout are not supported.
553
+ - The page has no scripts and a Content-Security-Policy that blocks all loads, so opening it
554
+ makes no network request. All text is escaped.
555
+ - The exit code is the plan's: 0 ready, 1 blocked/review, 6 key/config, 7 rescan found gating
556
+ findings. Use `sanitize` and `rescan` as the CI gate. Up to 500 records are shown, each cut
557
+ to 4,000 characters; the counts include all records. It takes a little longer than
558
+ `sanitize` (about 15% on a 40,000-line synthetic log).
559
+
488
560
  ### Troubleshooting
489
561
 
490
562
  - `destination_required_but_missing` — pass `--destination` (and `--environment`).
@@ -11,7 +11,7 @@ vendor ticket. CipherGate replaces that with a local, explainable, scriptable ga
11
11
  inspect -> plan -> sanitize --apply -> rescan
12
12
  ```
13
13
 
14
- > **Status: 0.15.0 (alpha).** No detector finds every sensitive value. A passing run means the
14
+ > **Status: 0.17.0 (alpha).** No detector finds every sensitive value. A passing run means the
15
15
  > configured processing completed under the selected policy; it is **not** proof that the output
16
16
  > contains no sensitive information, and it is not a compliance (GDPR, HIPAA, PCI DSS, ...) or
17
17
  > anonymization guarantee. Policy templates must be reviewed before use.
@@ -52,6 +52,8 @@ inspect -> plan -> sanitize --apply -> rescan
52
52
  and its source, gates, key *status* (never key material), no raw values or fingerprints.
53
53
  A failed run's report lists `next_steps` as `{"code", "message"}` pairs, the same hints the
54
54
  terminal prints as `next:` lines, so CI can act on them.
55
+ - **Review page:** `ciphergate review` writes one local HTML file that shows what sanitize
56
+ would change, record by record, with run metrics. See [Review page](#review-page).
55
57
 
56
58
  ## Install
57
59
 
@@ -191,14 +193,40 @@ controlled pipeline configuration.
191
193
  ## Shipped policy templates (review before use)
192
194
 
193
195
  `dev_local`, `stg_test`, `prd_external_ai`, `developer_logs`, `test_dataset`, `external_ai`,
194
- `healthcare_template`, `financial_template`, `local_debug`, and `multi_environment` (one policy
196
+ `healthcare_template`, `financial_template`, `local_debug`, `hospital_us_override` (a layered example, see below), and `multi_environment` (one policy
195
197
  whose outcomes vary by environment *and* destination). All validate in CI. Templates named for
196
198
  healthcare or finance are starting points, **not** HIPAA/PCI controls.
197
199
 
200
+ ### Policy layering
201
+
202
+ A policy can `extends:` shipped packs, so a team keeps only its own differences in a small file:
203
+
204
+ ```text
205
+ healthcare_baseline (global strict default) -> us_hipaa (US identifier list) -> your file
206
+ ```
207
+
208
+ ```bash
209
+ ciphergate init --template hospital_us_override --output my-hospital.yml
210
+ ciphergate validate-policy my-hospital.yml
211
+ # layers: healthcare_baseline -> us_hipaa -> hospital-us-override
212
+ ```
213
+
214
+ A later layer can add context, detectors, classifications and stricter rules. It cannot quietly
215
+ weaken an earlier one: a weaker setting, a disabled detector or a rule that would override a
216
+ block, review or redaction with something weaker fails validation unless the rule names what it
217
+ relaxes (`relaxes: [rule-id]`), which is then reported as a warning. Hard and guardrail rules
218
+ can never be relaxed. Reports list every layer with its digest and show which layer decided
219
+ each value. Jurisdiction packs: `us_hipaa` (modelled on the HIPAA Safe Harbor identifier list
220
+ but **not** Safe Harbor or Expert Determination by itself), `eu_gdpr` (GDPR special categories
221
+ and member-state identifiers) and `india_dpdp` (Aadhaar, ABHA, PAN and sensitive fields). None
222
+ of them is legal advice or makes processing lawful. A policy can extend several. See
223
+ [docs/policy-schema.md](docs/policy-schema.md#layering-extends).
224
+
198
225
  ## Commands and exit codes
199
226
 
200
- `init`, `validate-policy`, `inspect`, `plan`, `sanitize` (needs `--apply`), `rescan`, `explain`
201
- (decision for a hypothetical finding — no data needed). All support `--help`.
227
+ `init`, `validate-policy`, `inspect`, `plan`, `sanitize` (needs `--apply`), `rescan`, `review`
228
+ (local HTML page, see below), `explain` (decision for a hypothetical finding — no data
229
+ needed). All support `--help`.
202
230
 
203
231
  Common options: `--policy`, `--format csv|json|jsonl|text|logfmt|jsonlog|accesslog|syslog|stacktrace`, `--report PATH`,
204
232
  `--report-format json|text` (JSON without `--report` prints only JSON on stdout),
@@ -446,6 +474,50 @@ kubectl logs my-pod | ciphergate sanitize - --format text --policy policies/prd_
446
474
  | 8 | Unexpected internal error (safe message) |
447
475
  | 130 | Interrupted (no output published) |
448
476
 
477
+ ### Review page
478
+
479
+ `ciphergate review` runs the same steps as `sanitize --apply`, including the rescan, but writes
480
+ no sanitized file. It writes one HTML page instead:
481
+
482
+ ```bash
483
+ ciphergate review app.log --policy policies/developer_logs.yml --environment dev --destination local_dev -o build/review.html
484
+ ```
485
+
486
+ Open the file in a browser. It is laid out like a trace: a tree on the left, details of the
487
+ selected item on the right.
488
+
489
+ - **review run:** a verdict, then metrics. These include findings and how many are
490
+ transformed, kept by an `allow` rule, need review or are blocked (with percentages), records
491
+ with findings, values changed, records dropped, rescan results and possible partial finds.
492
+ - **Steps**, with measured timings: the plan pass (read input, detect, decide), transform,
493
+ rescan and output.
494
+ - *detect* shows counts by category, detector and confidence.
495
+ - *decide* shows the policy and context, what happened to each category, counts by action,
496
+ rule and reason code, plus gates and key status.
497
+ - **Records with findings:** each one opens as Input and Output panels with a decisions table.
498
+ Changed parts are highlighted, and hovering over one shows its category, action, rule and
499
+ reason code. Values kept by an `allow` rule are underlined. A dashed outline marks a
500
+ *possible partial find*: a replaced name or address next to a capitalised word that stayed.
501
+ This is a hint, not a check.
502
+
503
+ What the page contains, and how to handle it:
504
+
505
+ - By default it contains the would-be **output**, not the input. Treat it like the sanitized
506
+ file: detectors can miss values.
507
+ - `--include-input` also adds the **raw input** next to the output, with a warning banner. A
508
+ page made with it must be handled like the original file. Do not share, upload or commit it.
509
+ - If the run is blocked, needs review or a key is missing, there is no output preview. The page
510
+ shows the plan and the decisions for each record. Planned transforms are labelled as not run.
511
+ - There are no default locations: `-o` is required. `build/` is in `.gitignore`. The page is not
512
+ overwritten without `--force`, can't be the input or the policy file, and is written
513
+ atomically (0600 on POSIX). Directories and stdin/stdout are not supported.
514
+ - The page has no scripts and a Content-Security-Policy that blocks all loads, so opening it
515
+ makes no network request. All text is escaped.
516
+ - The exit code is the plan's: 0 ready, 1 blocked/review, 6 key/config, 7 rescan found gating
517
+ findings. Use `sanitize` and `rescan` as the CI gate. Up to 500 records are shown, each cut
518
+ to 4,000 characters; the counts include all records. It takes a little longer than
519
+ `sanitize` (about 15% on a 40,000-line synthetic log).
520
+
449
521
  ### Troubleshooting
450
522
 
451
523
  - `destination_required_but_missing` — pass `--destination` (and `--environment`).
@@ -33,6 +33,7 @@ Report (reporting/) privacy-safe JSON / text summary
33
33
  | `readers/` | Parse and write each format; limits; encodings; JSON Pointer paths | decide actions |
34
34
  | `detectors/` | Produce candidate findings; per-run `DetectorSet` (no global registry) | decide actions |
35
35
  | `policy/loader.py` | Safe YAML (no tags, no aliases, no duplicate keys, size limit) | execute code |
36
+ | `policy/layers.py` | Resolve `extends` (shipped packs by name), merge layers, reject weakening | read local files by name |
36
37
  | `policy/validator.py` | Schema + semantic checks, compile into immutable `Policy` | — |
37
38
  | `policy/precedence.py` | Computed precedence levels and static overlap analysis | — |
38
39
  | `policy/context.py` | Pinned vs caller context, run-level gates | infer one context dimension from another |
@@ -40,6 +41,7 @@ Report (reporting/) privacy-safe JSON / text summary
40
41
  | `transforms/` | Execute approved actions; HMAC pseudonyms; key lookup | log values or keys |
41
42
  | `pipeline/` | Orchestration, two passes, rescan, atomic output, path safety | publish on any failure |
42
43
  | `reporting/` | Report building, control-char escaping, field-path masking, rendering | include values/fingerprints |
44
+ | `pipeline/review.py`, `reporting/review_page.py` | `review`: sanitize steps without publishing, per-record changes, local HTML page | publish output, add input without `--include-input`, load anything |
43
45
  | `cli.py` | Typer CLI, exit codes, safe error rendering, SIGTERM -> interrupt | enable rich tracebacks (they print locals) |
44
46
 
45
47
  ## Why two passes
@@ -32,8 +32,31 @@ in `tests/unit/test_presidio_ner.py` use a fake engine. `tests/integration/test_
32
32
  runs the real Presidio with `en_core_web_sm`: it finds a common synthetic name and place, ignores
33
33
  text with no entities, removes them in a `sanitize` run with network access disabled, and fails
34
34
  closed when the model is missing. CI runs it in the `ner` job (not a required check). These are
35
- smoke tests on a few sentences: accuracy on real text has not been measured by this project,
36
- and the small model misses names that the larger `en_core_web_lg` may find.
35
+ smoke tests on a few sentences.
36
+
37
+ `scripts/benchmark/ner_names.py` measures name detection on 320 generated log lines (8 regions x
38
+ 5 ways of writing a name x 8 lines) plus 16 name-free lines. With `en_core_web_sm` (spaCy 3.8,
39
+ Presidio 2.2.366), seeds 7 and 11:
40
+
41
+ | | seed 7 | seed 11 |
42
+ |---|---|---|
43
+ | names with any part found | 184/320 (57%) | 190/320 (59%) |
44
+ | names found in full | 130/320 (41%) | 132/320 (41%) |
45
+ | name-free lines flagged | 3/16 | 3/16 |
46
+
47
+ - When only part of a name is found, the rest stays in the output (for example the family name).
48
+ - `First Last` and `Last, First` are found most often (50-61 of 64). Lowercase (27/64) and
49
+ upper-case (15/64) names are mostly missed.
50
+ - English, Chinese and German names were found more often (27-35 of 40) than Indian and West
51
+ African names (12-17 of 40).
52
+ - The false alarms were product names (`Kafka`, `Jenkins`, `Grafana`).
53
+ - `min_score` from 0.4 to 0.85 changed nothing: Presidio gives these spaCy entities one fixed
54
+ score, so the threshold cannot trade misses against false alarms with this model.
55
+ - The default `en_core_web_lg` was not measured. Run the script with `--model en_core_web_lg`
56
+ after installing it.
57
+
58
+ These are synthetic lines written by the project, not a public accuracy claim. Treat the NER
59
+ detector as an extra layer: do not rely on it alone to remove names, especially from free text.
37
60
 
38
61
  ## Text/log specifics
39
62
 
@@ -17,6 +17,23 @@
17
17
  - Environment, target environment and destination are declared by the caller or policy and are
18
18
  not verified. CipherGate does not inspect cloud identity or control deployments.
19
19
 
20
+ ## Policy layering
21
+
22
+ - `extends` names shipped packs only; a policy cannot extend a local file in this release.
23
+ - The weakening check compares rule selectors statically and conservatively: it can reject a
24
+ rule that would never actually override the earlier one (narrow the selectors or use
25
+ `relaxes`), and it cannot see differences that depend on detector behaviour.
26
+ - The `us_hipaa` pack does not implement the 3-digit ZIP allowance (whole ZIP codes are
27
+ redacted), cannot check the Safe Harbor "no actual knowledge" condition, does not parse
28
+ photos or biometric data, and cannot know every "other unique identifying number". It is
29
+ not Safe Harbor or Expert Determination by itself.
30
+ - The `eu_gdpr` pack finds most national identifiers by field name only (the Italian codice
31
+ fiscale also by value). It cannot decide a lawful basis, an Art. 9(2) condition or whether a
32
+ destination is outside the EEA.
33
+ - The `india_dpdp` pack finds Aadhaar numbers by value only when written in groups and does not
34
+ verify the Verhoeff checksum; voter ID, passport and licence numbers are found by field name
35
+ only. It cannot check consent, purpose limitation, retention or cross-border rules.
36
+
20
37
  ## Formats and processing
21
38
 
22
39
  - JSON is parsed as a whole document (default limit 32 MiB). CSV/JSONL/text are streamed with
@@ -10,6 +10,7 @@ Validate with `ciphergate validate-policy FILE` (never reads data).
10
10
  |---|---|---|---|
11
11
  | `policy_version` | `1` | required | other versions rejected |
12
12
  | `name` | slug `[a-z0-9][a-z0-9_.-]{0,63}` | required | also the default pseudonym namespace |
13
+ | `extends` | list of shipped pack names (max 4) | `[]` | build on [policy packs](#layering-extends) |
13
14
  | `metadata` | mapping | `{}` | free-form extension namespace; not interpreted |
14
15
  | `context` | [Context](#context) | all unset | pinned context |
15
16
  | `requirements` | `{environment, target_environment, destination}` booleans | `true, false, true` | which context must be present (a run without a destination is still treated as untrusted; turning the requirement off warns) |
@@ -181,6 +182,77 @@ future. Requires `strict: true` and the rescan gate; validation always warns, an
181
182
  expire automatically — review them. No shipped policy contains an
182
183
  approval. Rules still apply — an approval does not allow raw data through.
183
184
 
185
+ ## Layering (`extends`)
186
+
187
+ A policy can build on shipped **packs** instead of repeating them:
188
+
189
+ ```yaml
190
+ policy_version: 1
191
+ name: my-hospital
192
+ extends: [us_hipaa] # us_hipaa itself extends healthcare_baseline
193
+ context: {environment: prd, target_environment: external, destination: external_ai}
194
+ classifications:
195
+ - {id: local-ids, field_names: [visit_number], category: pii.customer_identifier}
196
+ ```
197
+
198
+ Shipped packs (`policies/packs/`, all **templates requiring review**):
199
+
200
+ | Pack | Extends | What it adds |
201
+ |---|---|---|
202
+ | `healthcare_baseline` | — | Strict default for healthcare-style data anywhere: credentials hard-blocked; clinical values blocked toward external/unknown destinations and sent to review elsewhere; identifiers redacted; DOB and clinical dates cut to the year; internal infrastructure redacted; everything else review. Needs no key. |
203
+ | `us_hipaa` | `healthcare_baseline` | Field classifications modelled on the 18 HIPAA Safe Harbor identifier types (geography, dates, ages, record/plan/account numbers, licenses, vehicles, devices, URLs, biometrics, photos); ages banded with 90 and over as `90+`. It is **not** Safe Harbor or Expert Determination by itself: the 3-digit ZIP allowance and the "no actual knowledge" condition are not implemented, binary photos/biometrics are not parsed, and free-text detection is incomplete. |
204
+ | `eu_gdpr` | `healthcare_baseline` | GDPR Art. 9 special-category fields (genetic, biometric, ethnic origin, religion, political opinion, trade union, sex life/orientation) blocked toward external/unknown destinations and reviewed elsewhere; national and health-insurance id fields from several member states plus an Italian codice fiscale value pattern; ages banded. It cannot decide a lawful basis or Chapter V transfers, and pseudonymised data is still personal data, so it redacts by default. |
205
+ | `india_dpdp` | `healthcare_baseline` | Aadhaar (field name, and value when grouped as `1234 5678 9012`; no Verhoeff check; unseparated 12 digits by field name only), ABHA numbers and `@abdm`/`@sbx` addresses, PAN (value and field), EPIC/passport/licence/ration card/UHID fields, PIN codes as address; SPDI-style sensitive fields blocked toward external/unknown destinations and reviewed elsewhere; ages banded. It cannot decide consent, purpose, retention or cross-border rules. |
206
+
207
+ `policies/hospital_us_override.yml` is an example third layer (`ciphergate init --template
208
+ hospital_us_override`). A policy may extend several jurisdiction packs at once (for example
209
+ `extends: [us_hipaa, eu_gdpr, india_dpdp]` for a hospital group); the strictest combination
210
+ applies.
211
+
212
+ **Resolution.** Names are compared to the shipped packs and never used as paths; local files
213
+ cannot be extended in this release. Packs may extend packs (at most 4 levels, 8 layers; cycles
214
+ are rejected). Layers are ordered base first, your policy last; a pack reached twice is used once.
215
+ Every check in this document then runs on the merged policy.
216
+
217
+ **Merge rules — a later layer can never quietly weaken an earlier one:**
218
+
219
+ | Part | Merge | Rejected (validation error) |
220
+ |---|---|---|
221
+ | `context` | later layers may pin unset fields | pinning a different value (`layer_context_conflict`); `allow_cli_override` for a field an earlier layer pinned without allowing overrides (`layer_weakens_context`) |
222
+ | `requirements` | any layer requiring a field requires it | setting `false` where an earlier layer has `true` (`layer_weakens_setting`) |
223
+ | `settings` | stricter value kept: `strict`, `fail_on_parse_error`, `scan_json_keys`, `rescan.enabled` (true), `unknown_sensitive_behavior` (`block` > `review`), `csv_duplicate_headers` (`reject`), `malformed_record_action` (unset); `rescan.fail_on_categories` lists are combined (an unset value contributes every high-risk category) | a weaker value (`layer_weakens_setting`) |
224
+ | `detectors` | custom detectors and internal host suffixes are added; enabling Presidio is allowed | disabling a detector or Presidio that an earlier layer runs, or turning off `reject_risky_regex` (`layer_weakens_detectors` / `layer_weakens_setting`) |
225
+ | `custom_categories`, `transfers` | combined | — |
226
+ | `classifications`, `rules` | combined | reusing an id from an earlier layer (`duplicate_id`) |
227
+ | `defaults` | a later default replaces an earlier one for the same selector | a later default that would take over a category from an earlier default with a weaker action (`layer_weakens_default`) |
228
+ | `name`, `metadata` | the last layer's | — |
229
+
230
+ **Rules.** Actions are ranked `block` > `review` > any transform > `allow`. A later rule that
231
+ [outranks](#precedence-deterministic) an earlier rule it may overlap with and picks a lower-ranked
232
+ action fails with `layer_weakens_rule`. To do it on purpose, name the earlier rule in
233
+ `relaxes`:
234
+
235
+ ```yaml
236
+ rules:
237
+ - id: hospital-keep-state
238
+ priority: 300
239
+ match: {categories: [location.coarse], field_names: [state]}
240
+ action: allow
241
+ relaxes: ["defaults:location.*"] # or an earlier rule id
242
+ ```
243
+
244
+ Each relaxation is printed as a validation warning and appears in every run report's warnings.
245
+ `hard` and `guardrail` rules from an earlier layer can **never** be relaxed. A later `allow` rule
246
+ over categories an earlier non-allow default handles needs `relaxes: ["defaults:<selector>"]`.
247
+ Swapping one transform for another (for example `pseudonymize` instead of `redact`) needs no
248
+ `relaxes`. `relaxes` is rejected in a policy without `extends`, and must name a rule or default
249
+ from an earlier layer.
250
+
251
+ **Provenance.** `validate-policy` prints the layer chain. Reports add `policy.layers` (each
252
+ layer's name, kind, source and file digest) and each decision's `layer`; `policy.digest` covers
253
+ every layer in order, so editing any pack changes it. With `--path-display redacted` your own
254
+ policy file name is hidden; shipped pack names are kept.
255
+
184
256
  ## Invalid examples
185
257
 
186
258
  ```yaml
@@ -75,6 +75,21 @@ values are replaced with `<redacted-key>`; control and bidi characters are escap
75
75
  Logging uses the standard library and never receives values. `--verbose` lists more items;
76
76
  `--debug` adds traceback frame locations for internal errors without exception messages.
77
77
 
78
+ ## Review page
79
+
80
+ `ciphergate review` is the only command that puts data into a file other than the output. By
81
+ default the page holds the would-be sanitized output, so it is as sensitive as that output.
82
+ With `--include-input` it also holds the raw input and must be handled like the input; the
83
+ page and the command's stderr both say so. The JSON report never contains either.
84
+
85
+ The would-be output is written to a `mkstemp` file in the page's directory, rescanned, and
86
+ always removed; it is never published. The page itself uses the same atomic,
87
+ no-overwrite-by-default writer as outputs and refuses the input and policy paths. It contains
88
+ no scripts. A Content-Security-Policy (`default-src 'none'`) and `no-referrer` stop any load
89
+ or request, and every text is HTML-escaped with control and bidi characters shown as escapes.
90
+ Field paths in the decision tables use the same masking as reports. The possible-partial-find
91
+ mark is a heuristic and can miss partial finds or flag whole ones.
92
+
78
93
  ## Files
79
94
 
80
95
  Inputs must be regular files (FIFOs/devices refused). A directory INPUT starts a batch run:
@@ -0,0 +1,46 @@
1
+ # TEMPLATE REQUIRING REVIEW. Example of the third policy layer: one hospital's own small file on
2
+ # top of the shipped packs (healthcare_baseline -> us_hipaa -> this file). Replace the synthetic
3
+ # formats and field names below with your own. Using it does NOT perform HIPAA Safe Harbor or
4
+ # Expert Determination and is not legal or compliance advice.
5
+ #
6
+ # A layer like this can add context, detectors, classifications and stricter rules. It cannot
7
+ # quietly weaken the packs: a weaker setting or a rule that would override a pack's block,
8
+ # review or redaction with something weaker fails validation unless it names that rule (or
9
+ # default) in `relaxes`, which is then printed as a warning and recorded in every report.
10
+ policy_version: 1
11
+ name: hospital-us-override
12
+ extends: [us_hipaa]
13
+ metadata:
14
+ description: "Example hospital layer for PRD logs going to an external AI assistant. Template requiring review."
15
+ context:
16
+ environment: prd
17
+ target_environment: external
18
+ destination: external_ai
19
+ use_case: debugging
20
+ requirements:
21
+ target_environment: true
22
+ detectors:
23
+ custom:
24
+ # Synthetic local record-number format; replace with the hospital's real format.
25
+ - id: hospital-record-number
26
+ category: pii.customer_identifier
27
+ pattern: '\bHRN-[0-9]{8}\b'
28
+ confidence: 0.9
29
+ classifications:
30
+ - id: hospital-local-ids
31
+ field_names: [visit_number, encounter_id, bed_assignment, wristband_id]
32
+ category: pii.customer_identifier
33
+ - id: hospital-state
34
+ field_names: [state, state_code]
35
+ category: location.coarse
36
+ rules:
37
+ # Safe Harbor keeps the state; the baseline redacts all locations by default, so keeping it
38
+ # is a deliberate, recorded relaxation.
39
+ - id: hospital-keep-state
40
+ priority: 300
41
+ match:
42
+ categories: [location.coarse]
43
+ field_names: [state, state_code]
44
+ action: allow
45
+ relaxes: ["defaults:location.*"]
46
+ reason: state_is_kept_for_regional_debugging
@@ -0,0 +1,78 @@
1
+ # TEMPLATE REQUIRING REVIEW. Layering pack for EU healthcare-style data, built on
2
+ # healthcare_baseline. It is modelled on GDPR concepts but using it does NOT make processing
3
+ # lawful and is not legal or compliance advice. GDPR has no fixed identifier list like HIPAA
4
+ # Safe Harbor: whether data is personal depends on whether a person is identifiable by any
5
+ # means reasonably likely to be used (Recital 26). Detection is incomplete, especially in free
6
+ # text and in languages other than English.
7
+ #
8
+ # What this pack adds on top of the baseline:
9
+ # * Art. 9 special categories (health is already covered by the baseline; this adds genetic,
10
+ # biometric, racial or ethnic origin, political opinions, religious or philosophical
11
+ # beliefs, trade union membership, sex life and sexual orientation fields): blocked toward
12
+ # external/unknown destinations, human review everywhere else;
13
+ # * national and health-insurance identifier fields from several member states, plus a value
14
+ # pattern for the Italian codice fiscale; other national numbers are found by field name only;
15
+ # * ages banded by 10 (90 and over as "90+"), like the other jurisdiction packs.
16
+ #
17
+ # Things this pack cannot decide for you:
18
+ # * a lawful basis and an Art. 9(2) condition for the processing;
19
+ # * Chapter V international transfers: an `external_ai` or `external_vendor` destination may
20
+ # be outside the EEA, and CipherGate does not know where a destination is;
21
+ # * whether a keyed pseudonym is acceptable: pseudonymised data is still personal data
22
+ # (Art. 4(5), Recital 26), so this pack redacts rather than pseudonymizes by default.
23
+ policy_version: 1
24
+ name: eu-gdpr
25
+ extends: [healthcare_baseline]
26
+ metadata:
27
+ description: "EU healthcare pack modelled on GDPR special categories and member-state identifiers. Template requiring review; not legal advice."
28
+ pack_version: 1
29
+ custom_categories:
30
+ - pii.age
31
+ - pii.special_category
32
+ - pii.biometric_identifier
33
+ detectors:
34
+ custom:
35
+ # Italian codice fiscale: 6 letters, 2 digits, month letter, 2 digits, letter, 3 digits, letter.
36
+ - id: eu-it-codice-fiscale
37
+ category: pii.government_id
38
+ pattern: '\b[A-Z]{6}[0-9]{2}[ABCDEHLMPRST][0-9]{2}[A-Z][0-9]{3}[A-Z]\b'
39
+ confidence: 0.85
40
+ classifications:
41
+ - id: eu-special-categories
42
+ field_names: [ethnicity, ethnic_origin, race, racial_origin, religion, religious_belief, philosophical_belief, political_opinion, political_party, party_affiliation, trade_union, union_membership, sexual_orientation, sex_life, gender_identity, genetic_data, genotype, dna_sequence, genetic_test_result]
43
+ category: pii.special_category
44
+ - id: eu-biometrics
45
+ field_names: [fingerprint, face_template, facial_image, iris_scan, retina_scan, voiceprint, biometric_template]
46
+ category: pii.biometric_identifier
47
+ - id: eu-national-ids
48
+ field_names: [codice_fiscale, steuer_id, steueridentifikationsnummer, bsn, burgerservicenummer, nir, numero_securite_sociale, dni, nie, nif, pesel, personnummer, henkilotunnus, cpr_number, rodne_cislo, ppsn, ehic_number, health_insurance_number, krankenversicherungsnummer, carte_vitale_number]
49
+ category: pii.government_id
50
+ - id: eu-age
51
+ field_names: [age, patient_age, age_years, alter, edad, eta]
52
+ category: pii.age
53
+ rules:
54
+ - id: eu-block-special-categories-external
55
+ priority: 800
56
+ match:
57
+ destinations: [external_vendor, external_ai, public_repository, unknown]
58
+ categories: [pii.special_category, pii.biometric_identifier]
59
+ action: block
60
+ reason: special_category_data_needs_review_before_external_release
61
+
62
+ - id: eu-review-special-categories
63
+ priority: 400
64
+ match:
65
+ categories: [pii.special_category, pii.biometric_identifier]
66
+ action: review
67
+ reason: special_category_data_requires_human_decision
68
+
69
+ - id: eu-age-band
70
+ priority: 300
71
+ match:
72
+ categories: [pii.age]
73
+ action: generalize
74
+ params:
75
+ strategy: age_band
76
+ band_size: 10
77
+ max_age: 90
78
+ reason: ages_banded