ciphergate-cli 0.14.0__tar.gz → 0.15.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 (151) hide show
  1. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/CHANGELOG.md +18 -1
  2. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/PKG-INFO +4 -2
  3. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/README.md +3 -1
  4. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/docs/security-and-privacy.md +2 -1
  5. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/__init__.py +1 -1
  6. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/models/report.py +6 -2
  7. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/pipeline/batch.py +6 -2
  8. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/reporting/next_steps.py +28 -9
  9. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/reporting/terminal.py +4 -10
  10. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/integration/test_batch.py +13 -0
  11. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/integration/test_cli_e2e.py +14 -0
  12. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_next_steps.py +33 -1
  13. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/.gitignore +0 -0
  14. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/LICENSE +0 -0
  15. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/NOTICE +0 -0
  16. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/SECURITY.md +0 -0
  17. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/docs/architecture.md +0 -0
  18. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/docs/claude-code-tooling.md +0 -0
  19. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/docs/detector-coverage.md +0 -0
  20. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/docs/examples.md +0 -0
  21. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/docs/limitations.md +0 -0
  22. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/docs/policy-schema.md +0 -0
  23. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/docs/threat-model.md +0 -0
  24. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/examples/expected/sample.dev_local_dev.csv +0 -0
  25. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/examples/expected/sample.dev_local_dev.json +0 -0
  26. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/examples/expected/sample.dev_local_dev.jsonl +0 -0
  27. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/examples/expected/sample.dev_local_dev.log +0 -0
  28. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/examples/sample.csv +0 -0
  29. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/examples/sample.json +0 -0
  30. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/examples/sample.jsonl +0 -0
  31. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/examples/sample.log +0 -0
  32. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/policies/dev_local.yml +0 -0
  33. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/policies/developer_logs.yml +0 -0
  34. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/policies/external_ai.yml +0 -0
  35. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/policies/financial_template.yml +0 -0
  36. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/policies/healthcare_template.yml +0 -0
  37. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/policies/local_debug.yml +0 -0
  38. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/policies/multi_environment.yml +0 -0
  39. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/policies/prd_external_ai.yml +0 -0
  40. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/policies/stg_test.yml +0 -0
  41. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/policies/test_dataset.yml +0 -0
  42. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/pyproject.toml +0 -0
  43. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/scripts/benchmark/README.md +0 -0
  44. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/__main__.py +0 -0
  45. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/cli.py +0 -0
  46. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/config.py +0 -0
  47. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/constants.py +0 -0
  48. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/__init__.py +0 -0
  49. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/base.py +0 -0
  50. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/custom_regex.py +0 -0
  51. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/dates.py +0 -0
  52. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/email.py +0 -0
  53. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/field_names.py +0 -0
  54. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/iban.py +0 -0
  55. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/ip_address.py +0 -0
  56. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/mac_address.py +0 -0
  57. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/network.py +0 -0
  58. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/payment_card.py +0 -0
  59. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/phone.py +0 -0
  60. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/presidio_ner.py +0 -0
  61. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/regex_safety.py +0 -0
  62. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/registry.py +0 -0
  63. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/secrets.py +0 -0
  64. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/detectors/ssn.py +0 -0
  65. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/errors.py +0 -0
  66. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/models/__init__.py +0 -0
  67. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/models/context.py +0 -0
  68. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/models/finding.py +0 -0
  69. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/models/policy.py +0 -0
  70. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/pipeline/__init__.py +0 -0
  71. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/pipeline/atomic_output.py +0 -0
  72. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/pipeline/inspector.py +0 -0
  73. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/pipeline/paths.py +0 -0
  74. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/pipeline/planner.py +0 -0
  75. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/pipeline/rescan.py +0 -0
  76. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/pipeline/sanitizer.py +0 -0
  77. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/pipeline/stdio.py +0 -0
  78. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/pipeline/streams.py +0 -0
  79. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/policy/__init__.py +0 -0
  80. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/policy/context.py +0 -0
  81. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/policy/engine.py +0 -0
  82. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/policy/loader.py +0 -0
  83. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/policy/precedence.py +0 -0
  84. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/policy/templates.py +0 -0
  85. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/policy/validator.py +0 -0
  86. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/py.typed +0 -0
  87. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/__init__.py +0 -0
  88. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/accesslog_reader.py +0 -0
  89. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/base.py +0 -0
  90. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/csv_reader.py +0 -0
  91. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/json_reader.py +0 -0
  92. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/jsonl_reader.py +0 -0
  93. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/jsonlog_reader.py +0 -0
  94. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/line_structure.py +0 -0
  95. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/logfmt_reader.py +0 -0
  96. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/stacktrace_reader.py +0 -0
  97. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/syslog_reader.py +0 -0
  98. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/readers/text_reader.py +0 -0
  99. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/reporting/__init__.py +0 -0
  100. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/reporting/audit.py +0 -0
  101. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/reporting/safe_text.py +0 -0
  102. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/reporting/serializer.py +0 -0
  103. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/templates/starter_policy.yml +0 -0
  104. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/transforms/__init__.py +0 -0
  105. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/transforms/date_shift.py +0 -0
  106. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/transforms/generalize.py +0 -0
  107. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/transforms/keys.py +0 -0
  108. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/transforms/pseudonymize.py +0 -0
  109. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/transforms/redact.py +0 -0
  110. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/transforms/registry.py +0 -0
  111. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/src/ciphergate/transforms/replace.py +0 -0
  112. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/__init__.py +0 -0
  113. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/conftest.py +0 -0
  114. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/integration/__init__.py +0 -0
  115. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/integration/test_api_batch_streams.py +0 -0
  116. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/integration/test_atomic_output.py +0 -0
  117. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/integration/test_domain_templates.py +0 -0
  118. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/integration/test_environment_matrix.py +0 -0
  119. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/integration/test_pipelines.py +0 -0
  120. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/integration/test_presidio_real.py +0 -0
  121. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/integration/test_stdio.py +0 -0
  122. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/regression/__init__.py +0 -0
  123. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/regression/test_known_cases.py +0 -0
  124. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/security/__init__.py +0 -0
  125. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/security/test_key_file.py +0 -0
  126. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/security/test_no_network.py +0 -0
  127. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/security/test_no_raw_value_leaks.py +0 -0
  128. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/security/test_path_safety.py +0 -0
  129. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/security/test_policy_fail_closed.py +0 -0
  130. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/security/test_review_findings.py +0 -0
  131. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/tooling/__init__.py +0 -0
  132. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/tooling/test_benchmark.py +0 -0
  133. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/tooling/test_ci_supply_chain.py +0 -0
  134. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/tooling/test_claude_tooling.py +0 -0
  135. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/tooling/test_git_tooling.py +0 -0
  136. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/tooling/test_publish_workflow.py +0 -0
  137. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/__init__.py +0 -0
  138. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_accesslog.py +0 -0
  139. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_detectors.py +0 -0
  140. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_jsonlog.py +0 -0
  141. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_logfmt.py +0 -0
  142. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_policy_engine.py +0 -0
  143. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_policy_precedence.py +0 -0
  144. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_policy_templates.py +0 -0
  145. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_policy_validation.py +0 -0
  146. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_presidio_ner.py +0 -0
  147. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_properties.py +0 -0
  148. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_readers.py +0 -0
  149. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_stacktrace.py +0 -0
  150. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_syslog.py +0 -0
  151. {ciphergate_cli-0.14.0 → ciphergate_cli-0.15.0}/tests/unit/test_transforms.py +0 -0
@@ -6,6 +6,22 @@ versioning: [PEP 440](https://peps.python.org/pep-0440/) with SemVer meaning (pr
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.15.0] - 2026-11-02
10
+
11
+ JSON reports now include the next-step hints, so CI pipelines can act on them. The field is new;
12
+ nothing existing changes.
13
+
14
+ ### Added
15
+
16
+ - JSON reports (single-file and directory) include `next_steps`: the hints from the terminal
17
+ summary as `{"code", "message"}` pairs, empty for a successful run. The field is new and
18
+ optional to read; the schema names are unchanged.
19
+
20
+ ### Fixed
21
+
22
+ - The `batch_empty` error for a directory with no recognised files now mentions
23
+ `--extension-format`.
24
+
9
25
  ## [0.14.0] - 2026-11-02
10
26
 
11
27
  Adds `--extension-format` for mixed directories and tests the optional NER detector against a
@@ -379,7 +395,8 @@ Pre-release (alpha). Licensed under Apache-2.0. Not a final release: the securit
379
395
  See `docs/limitations.md`. Deferred: optional NER/Presidio detectors, stdin/stdout streaming,
380
396
  directory processing, structured log parsers, key files, reversible token vault, lossy decoding.
381
397
 
382
- [Unreleased]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.14.0...HEAD
398
+ [Unreleased]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.15.0...HEAD
399
+ [0.15.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.14.0...v0.15.0
383
400
  [0.14.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.13.0...v0.14.0
384
401
  [0.13.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.12.0...v0.13.0
385
402
  [0.12.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.11.0...v0.12.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ciphergate-cli
3
- Version: 0.14.0
3
+ Version: 0.15.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.14.0 (alpha).** No detector finds every sensitive value. A passing run means the
53
+ > **Status: 0.15.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.
@@ -89,6 +89,8 @@ inspect -> plan -> sanitize --apply -> rescan
89
89
  `--force`, input/output alias protection, input-change detection, cleanup on failure/Ctrl-C.
90
90
  - **Reports:** counts by category/detector/action/reason/rule, safe locations, effective context
91
91
  and its source, gates, key *status* (never key material), no raw values or fingerprints.
92
+ A failed run's report lists `next_steps` as `{"code", "message"}` pairs, the same hints the
93
+ terminal prints as `next:` lines, so CI can act on them.
92
94
 
93
95
  ## Install
94
96
 
@@ -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.14.0 (alpha).** No detector finds every sensitive value. A passing run means the
14
+ > **Status: 0.15.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.
@@ -50,6 +50,8 @@ inspect -> plan -> sanitize --apply -> rescan
50
50
  `--force`, input/output alias protection, input-change detection, cleanup on failure/Ctrl-C.
51
51
  - **Reports:** counts by category/detector/action/reason/rule, safe locations, effective context
52
52
  and its source, gates, key *status* (never key material), no raw values or fingerprints.
53
+ A failed run's report lists `next_steps` as `{"code", "message"}` pairs, the same hints the
54
+ terminal prints as `next:` lines, so CI can act on them.
53
55
 
54
56
  ## Install
55
57
 
@@ -67,7 +67,8 @@ Reports contain: run ID, UTC time, version, input basename (or `<redacted>`), fo
67
67
  record counts, policy name/version/SHA-256 digest of the policy file, effective context with
68
68
  sources, gates, counts by category/detector/action/band/reason/rule, safe locations (record,
69
69
  line, field path, character span), rule IDs and reason codes, key status, changes, parse error
70
- line numbers and codes, rescan summary, warnings and a disclaimer. They never contain values,
70
+ line numbers and codes, rescan summary, warnings, `next_steps` (fixed hint text with a reason
71
+ code, for failed runs only) and a disclaimer. They never contain values,
71
72
  snippets, keys, mappings, or hashes of values. Field paths whose segments look like sensitive
72
73
  values are replaced with `<redacted-key>`; control and bidi characters are escaped.
73
74
 
@@ -24,7 +24,7 @@ Supported public API (everything else is internal and may change without notice)
24
24
  rescan_directory(root, policy, context=None, *, recursive=False, strict=None) -> BatchReport
25
25
  """
26
26
 
27
- __version__ = "0.14.0"
27
+ __version__ = "0.15.0"
28
28
 
29
29
  from ciphergate.config import RuntimeSettings
30
30
  from ciphergate.constants import (
@@ -11,6 +11,7 @@ from enum import StrEnum
11
11
  from typing import Any
12
12
 
13
13
  from ciphergate.constants import DISCLAIMER, REPORT_SCHEMA, ExitCode
14
+ from ciphergate.reporting.next_steps import next_step_items
14
15
 
15
16
 
16
17
  class RunStatus(StrEnum):
@@ -89,7 +90,7 @@ class RunReport:
89
90
  return self.exit_code == 0
90
91
 
91
92
  def to_dict(self) -> dict[str, Any]:
92
- return {
93
+ data: dict[str, Any] = {
93
94
  "schema": REPORT_SCHEMA,
94
95
  "command": self.command,
95
96
  "status": str(self.status),
@@ -111,5 +112,8 @@ class RunReport:
111
112
  "output": self.output,
112
113
  "rescan": self.rescan.to_dict() if self.rescan else None,
113
114
  "warnings": self.warnings,
114
- "disclaimer": DISCLAIMER,
115
115
  }
116
+ # Fixed hint text chosen by reason code: never contains input values.
117
+ data["next_steps"] = next_step_items(data)
118
+ data["disclaimer"] = DISCLAIMER
119
+ return data
@@ -52,6 +52,7 @@ from ciphergate.pipeline.sanitizer import (
52
52
  from ciphergate.policy.validator import Policy
53
53
  from ciphergate.readers import EXTENSION_FORMATS
54
54
  from ciphergate.reporting.audit import new_run_id, utc_now
55
+ from ciphergate.reporting.next_steps import merge_next_steps
55
56
 
56
57
  logger = logging.getLogger("ciphergate")
57
58
 
@@ -136,6 +137,7 @@ class BatchReport:
136
137
 
137
138
  def to_dict(self) -> dict[str, Any]:
138
139
  first = next((r.report for r in self.files if r.report is not None), None)
140
+ files = [r.to_dict() for r in self.files]
139
141
  return {
140
142
  "schema": BATCH_REPORT_SCHEMA,
141
143
  "command": self.command,
@@ -148,10 +150,11 @@ class BatchReport:
148
150
  "policy": first.policy if first is not None else None,
149
151
  "context": first.context if first is not None else None,
150
152
  "summary": self.summary(),
151
- "files": [r.to_dict() for r in self.files],
153
+ "files": files,
152
154
  "skipped": self.skipped,
153
155
  "output": self.output,
154
156
  "warnings": self.warnings,
157
+ "next_steps": merge_next_steps([f["report"] for f in files if "report" in f]),
155
158
  "disclaimer": DISCLAIMER,
156
159
  }
157
160
 
@@ -324,7 +327,8 @@ def coverage_warnings(
324
327
  def _require_files(discovery: Discovery) -> None:
325
328
  if not discovery.files:
326
329
  raise InputFormatError(
327
- "no supported input files found in the directory (see --format, --recursive)",
330
+ "no supported input files found in the directory (see --extension-format, --format, "
331
+ "--recursive)",
328
332
  code="batch_empty",
329
333
  )
330
334
 
@@ -83,7 +83,12 @@ _KEY_HINT = (
83
83
 
84
84
 
85
85
  def next_steps(report: dict[str, Any]) -> list[str]:
86
- """Hints for a run that ended with a non-zero exit code; empty otherwise."""
86
+ """Hint messages for a run that ended with a non-zero exit code; empty otherwise."""
87
+ return [step["message"] for step in next_step_items(report)]
88
+
89
+
90
+ def next_step_items(report: dict[str, Any]) -> list[dict[str, str]]:
91
+ """``{"code", "message"}`` pairs for a failed run; ``code`` is a stable reason code."""
87
92
  if not report.get("exit_code"):
88
93
  return []
89
94
  codes = [gate["reason_code"] for gate in report.get("gates", [])]
@@ -93,14 +98,28 @@ def next_steps(report: dict[str, Any]) -> list[str]:
93
98
  rescan = report.get("rescan")
94
99
  if rescan and rescan.get("failed"):
95
100
  codes.append(ReasonCode.RESIDUAL_FINDING)
96
- hints: list[str] = []
101
+ items: list[dict[str, str]] = []
97
102
  for code in codes:
98
103
  hint = _HINTS.get(code)
99
- if hint and hint not in hints:
100
- hints.append(hint)
104
+ if hint:
105
+ _add(items, str(code), hint)
101
106
  for key in report.get("keys", []):
102
- if key.get("status") != "available":
103
- hint = _KEY_HINT.format(env=key["key_env"])
104
- if hint not in hints:
105
- hints.append(hint)
106
- return hints
107
+ status = key.get("status")
108
+ if status != "available":
109
+ code = ReasonCode.KEY_MISSING if status == "missing" else ReasonCode.KEY_INVALID
110
+ _add(items, str(code), _KEY_HINT.format(env=key["key_env"]))
111
+ return items
112
+
113
+
114
+ def merge_next_steps(reports: list[dict[str, Any]]) -> list[dict[str, str]]:
115
+ """Combine the hints of several reports (a directory run), first occurrence first."""
116
+ items: list[dict[str, str]] = []
117
+ for report in reports:
118
+ for step in report.get("next_steps") or next_step_items(report):
119
+ _add(items, step["code"], step["message"])
120
+ return items
121
+
122
+
123
+ def _add(items: list[dict[str, str]], code: str, message: str) -> None:
124
+ if all(step["message"] != message for step in items):
125
+ items.append({"code": code, "message": message})
@@ -7,7 +7,6 @@ from typing import Any
7
7
  from ciphergate.constants import DISCLAIMER, RESCAN_LIMIT
8
8
  from ciphergate.models.report import RunReport
9
9
  from ciphergate.pipeline.batch import BatchReport
10
- from ciphergate.reporting.next_steps import next_steps
11
10
  from ciphergate.reporting.safe_text import escape_control
12
11
 
13
12
  SUMMARY_ITEMS = 15
@@ -120,8 +119,8 @@ def render_summary(report: RunReport, *, verbose: bool = False) -> str:
120
119
  lines.append(f" output: not written ({out['withheld_reason']})")
121
120
  for warning in d["warnings"]:
122
121
  lines.append(f" warning: {escape_control(warning)}")
123
- for hint in next_steps(d):
124
- lines.append(f" next: {hint}")
122
+ for step in d["next_steps"]:
123
+ lines.append(f" next: {step['message']}")
125
124
  lines.append(f" note: {DISCLAIMER}")
126
125
  return "\n".join(lines) + "\n"
127
126
 
@@ -161,13 +160,8 @@ def render_batch_summary(report: BatchReport, *, verbose: bool = False) -> str:
161
160
  lines.append(f" output: not written ({out.get('withheld_reason')})")
162
161
  for warning in d["warnings"]:
163
162
  lines.append(f" warning: {escape_control(warning)}")
164
- hints: list[str] = []
165
- for entry in d["files"]:
166
- for hint in next_steps(entry.get("report") or {}):
167
- if hint not in hints:
168
- hints.append(hint)
169
- for hint in hints:
170
- lines.append(f" next: {hint}")
163
+ for step in d["next_steps"]:
164
+ lines.append(f" next: {step['message']}")
171
165
  lines.append(" note: per-file details are in the JSON report (--report).")
172
166
  lines.append(f" note: {DISCLAIMER}")
173
167
  return "\n".join(lines) + "\n"
@@ -405,6 +405,11 @@ def test_directory_summary_carries_next_steps(tmp_path: Path) -> None:
405
405
  (root / "app.log").write_text(SECRET_LINE, encoding="utf-8")
406
406
  r = invoke("inspect", root, "--policy", POLICIES / "developer_logs.yml")
407
407
  assert r.exit_code == 1 and "next: pass --environment" in r.stdout
408
+ report = tmp_path / "report.json"
409
+ invoke("inspect", root, "--policy", POLICIES / "developer_logs.yml", "--report", report)
410
+ data = _report(report)
411
+ assert "environment_required_but_missing" in [s["code"] for s in data["next_steps"]]
412
+ assert data["next_steps"] == data["files"][0]["report"]["next_steps"]
408
413
 
409
414
 
410
415
  # --------------------------------------------------------------------------- --extension-format
@@ -496,3 +501,11 @@ def test_sanitize_directory_with_extension_format(tmp_path: Path, env) -> None:
496
501
  assert r.exit_code == 0, r.stdout
497
502
  assert sorted(p.name for p in out.iterdir()) == ["app.log", "users.dat"]
498
503
  assert "alice@example.com" not in (out / "users.dat").read_text(encoding="utf-8")
504
+
505
+
506
+ def test_empty_directory_error_mentions_extension_format(tmp_path: Path, env) -> None:
507
+ root = tmp_path / "in"
508
+ root.mkdir()
509
+ (root / "users.dat").write_text("email\nx\n", encoding="utf-8")
510
+ r = invoke("inspect", root, *PRD_EXTERNAL, env=env)
511
+ assert r.exit_code == 4 and "--extension-format" in r.stdout + r.stderr
@@ -206,6 +206,20 @@ def test_blocked_run_says_what_to_do_next(tmp_path: Path) -> None:
206
206
  assert "next: pass --environment" in r.stdout and "next: pass --destination" in r.stdout
207
207
 
208
208
 
209
+ def test_json_report_lists_next_steps(tmp_path: Path) -> None:
210
+ src = tmp_path / "raw.csv"
211
+ shutil.copy(EXAMPLES / "sample.csv", src)
212
+ report = tmp_path / "report.json"
213
+ r = invoke("inspect", src, "--policy", POLICIES / "developer_logs.yml", "--report", report)
214
+ assert r.exit_code == 1
215
+ steps = json.loads(report.read_text(encoding="utf-8"))["next_steps"]
216
+ codes = [s["code"] for s in steps]
217
+ assert codes[:2] == ["environment_required_but_missing", "destination_required_but_missing"]
218
+ assert len(codes) == len(set(codes))
219
+ for step in steps:
220
+ assert f"next: {step['message']}" in r.stdout
221
+
222
+
209
223
  def test_successful_run_has_no_next_steps(tmp_path: Path) -> None:
210
224
  clean = tmp_path / "clean.log"
211
225
  clean.write_text("service started\n", encoding="utf-8")
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from typing import Any
4
4
 
5
5
  from ciphergate.constants import ReasonCode
6
- from ciphergate.reporting.next_steps import _HINTS, next_steps
6
+ from ciphergate.reporting.next_steps import _HINTS, merge_next_steps, next_step_items, next_steps
7
7
 
8
8
 
9
9
  def report(**extra: Any) -> dict[str, Any]:
@@ -60,3 +60,35 @@ def test_hints_are_fixed_text() -> None:
60
60
  # Hints come from a fixed table, so they can never carry input values.
61
61
  unknown = {"by_reason_code": {"explicit_policy_match": 3, "not_a_code": 1}}
62
62
  assert next_steps(report(summary=unknown)) == []
63
+
64
+
65
+ def test_items_carry_stable_codes() -> None:
66
+ items = next_step_items(
67
+ report(
68
+ gates=[{"reason_code": ReasonCode.ENVIRONMENT_REQUIRED_BUT_MISSING}],
69
+ keys=[
70
+ {"key_env": "CIPHERGATE_HMAC_KEY", "status": "missing"},
71
+ {"key_env": "OTHER_KEY", "status": "too_short"},
72
+ ],
73
+ )
74
+ )
75
+ assert [i["code"] for i in items] == [
76
+ "environment_required_but_missing",
77
+ "key_missing",
78
+ "key_invalid",
79
+ ]
80
+ assert all(set(i) == {"code", "message"} for i in items)
81
+
82
+
83
+ def test_merge_keeps_first_occurrence_order() -> None:
84
+ a = report(gates=[{"reason_code": ReasonCode.DESTINATION_REQUIRED_BUT_MISSING}])
85
+ b = report(
86
+ gates=[
87
+ {"reason_code": ReasonCode.ENVIRONMENT_REQUIRED_BUT_MISSING},
88
+ {"reason_code": ReasonCode.DESTINATION_REQUIRED_BUT_MISSING},
89
+ ]
90
+ )
91
+ assert [i["code"] for i in merge_next_steps([a, b])] == [
92
+ "destination_required_but_missing",
93
+ "environment_required_but_missing",
94
+ ]
File without changes
File without changes