ciphergate-cli 0.9.0__tar.gz → 0.10.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 (148) hide show
  1. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/CHANGELOG.md +21 -1
  2. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/PKG-INFO +2 -2
  3. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/README.md +1 -1
  4. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/docs/detector-coverage.md +3 -2
  5. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/docs/policy-schema.md +1 -1
  6. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/pyproject.toml +2 -0
  7. ciphergate_cli-0.10.0/scripts/benchmark/README.md +39 -0
  8. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/__init__.py +1 -1
  9. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/payment_card.py +17 -2
  10. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/phone.py +5 -1
  11. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/registry.py +14 -1
  12. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/secrets.py +62 -2
  13. ciphergate_cli-0.10.0/src/ciphergate/detectors/ssn.py +45 -0
  14. ciphergate_cli-0.10.0/tests/tooling/test_benchmark.py +76 -0
  15. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_detectors.py +139 -0
  16. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/.gitignore +0 -0
  17. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/LICENSE +0 -0
  18. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/NOTICE +0 -0
  19. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/SECURITY.md +0 -0
  20. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/docs/architecture.md +0 -0
  21. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/docs/claude-code-tooling.md +0 -0
  22. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/docs/examples.md +0 -0
  23. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/docs/limitations.md +0 -0
  24. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/docs/security-and-privacy.md +0 -0
  25. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/docs/threat-model.md +0 -0
  26. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/examples/expected/sample.dev_local_dev.csv +0 -0
  27. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/examples/expected/sample.dev_local_dev.json +0 -0
  28. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/examples/expected/sample.dev_local_dev.jsonl +0 -0
  29. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/examples/expected/sample.dev_local_dev.log +0 -0
  30. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/examples/sample.csv +0 -0
  31. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/examples/sample.json +0 -0
  32. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/examples/sample.jsonl +0 -0
  33. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/examples/sample.log +0 -0
  34. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/policies/dev_local.yml +0 -0
  35. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/policies/developer_logs.yml +0 -0
  36. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/policies/external_ai.yml +0 -0
  37. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/policies/financial_template.yml +0 -0
  38. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/policies/healthcare_template.yml +0 -0
  39. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/policies/local_debug.yml +0 -0
  40. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/policies/multi_environment.yml +0 -0
  41. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/policies/prd_external_ai.yml +0 -0
  42. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/policies/stg_test.yml +0 -0
  43. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/policies/test_dataset.yml +0 -0
  44. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/__main__.py +0 -0
  45. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/cli.py +0 -0
  46. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/config.py +0 -0
  47. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/constants.py +0 -0
  48. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/__init__.py +0 -0
  49. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/base.py +0 -0
  50. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/custom_regex.py +0 -0
  51. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/dates.py +0 -0
  52. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/email.py +0 -0
  53. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/field_names.py +0 -0
  54. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/iban.py +0 -0
  55. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/ip_address.py +0 -0
  56. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/mac_address.py +0 -0
  57. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/network.py +0 -0
  58. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/presidio_ner.py +0 -0
  59. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/regex_safety.py +0 -0
  60. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/errors.py +0 -0
  61. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/models/__init__.py +0 -0
  62. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/models/context.py +0 -0
  63. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/models/finding.py +0 -0
  64. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/models/policy.py +0 -0
  65. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/models/report.py +0 -0
  66. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/__init__.py +0 -0
  67. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/atomic_output.py +0 -0
  68. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/batch.py +0 -0
  69. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/inspector.py +0 -0
  70. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/paths.py +0 -0
  71. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/planner.py +0 -0
  72. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/rescan.py +0 -0
  73. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/sanitizer.py +0 -0
  74. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/stdio.py +0 -0
  75. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/streams.py +0 -0
  76. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/__init__.py +0 -0
  77. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/context.py +0 -0
  78. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/engine.py +0 -0
  79. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/loader.py +0 -0
  80. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/precedence.py +0 -0
  81. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/templates.py +0 -0
  82. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/validator.py +0 -0
  83. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/py.typed +0 -0
  84. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/__init__.py +0 -0
  85. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/accesslog_reader.py +0 -0
  86. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/base.py +0 -0
  87. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/csv_reader.py +0 -0
  88. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/json_reader.py +0 -0
  89. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/jsonl_reader.py +0 -0
  90. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/jsonlog_reader.py +0 -0
  91. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/line_structure.py +0 -0
  92. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/logfmt_reader.py +0 -0
  93. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/stacktrace_reader.py +0 -0
  94. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/syslog_reader.py +0 -0
  95. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/text_reader.py +0 -0
  96. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/reporting/__init__.py +0 -0
  97. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/reporting/audit.py +0 -0
  98. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/reporting/safe_text.py +0 -0
  99. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/reporting/serializer.py +0 -0
  100. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/reporting/terminal.py +0 -0
  101. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/templates/starter_policy.yml +0 -0
  102. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/__init__.py +0 -0
  103. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/date_shift.py +0 -0
  104. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/generalize.py +0 -0
  105. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/keys.py +0 -0
  106. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/pseudonymize.py +0 -0
  107. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/redact.py +0 -0
  108. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/registry.py +0 -0
  109. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/replace.py +0 -0
  110. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/__init__.py +0 -0
  111. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/conftest.py +0 -0
  112. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/integration/__init__.py +0 -0
  113. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/integration/test_api_batch_streams.py +0 -0
  114. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/integration/test_atomic_output.py +0 -0
  115. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/integration/test_batch.py +0 -0
  116. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/integration/test_cli_e2e.py +0 -0
  117. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/integration/test_domain_templates.py +0 -0
  118. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/integration/test_environment_matrix.py +0 -0
  119. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/integration/test_pipelines.py +0 -0
  120. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/integration/test_stdio.py +0 -0
  121. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/regression/__init__.py +0 -0
  122. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/regression/test_known_cases.py +0 -0
  123. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/security/__init__.py +0 -0
  124. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/security/test_key_file.py +0 -0
  125. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/security/test_no_network.py +0 -0
  126. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/security/test_no_raw_value_leaks.py +0 -0
  127. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/security/test_path_safety.py +0 -0
  128. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/security/test_policy_fail_closed.py +0 -0
  129. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/security/test_review_findings.py +0 -0
  130. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/tooling/__init__.py +0 -0
  131. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/tooling/test_ci_supply_chain.py +0 -0
  132. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/tooling/test_claude_tooling.py +0 -0
  133. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/tooling/test_git_tooling.py +0 -0
  134. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/tooling/test_publish_workflow.py +0 -0
  135. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/__init__.py +0 -0
  136. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_accesslog.py +0 -0
  137. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_jsonlog.py +0 -0
  138. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_logfmt.py +0 -0
  139. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_policy_engine.py +0 -0
  140. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_policy_precedence.py +0 -0
  141. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_policy_templates.py +0 -0
  142. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_policy_validation.py +0 -0
  143. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_presidio_ner.py +0 -0
  144. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_properties.py +0 -0
  145. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_readers.py +0 -0
  146. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_stacktrace.py +0 -0
  147. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_syslog.py +0 -0
  148. {ciphergate_cli-0.9.0 → ciphergate_cli-0.10.0}/tests/unit/test_transforms.py +0 -0
@@ -6,6 +6,25 @@ versioning: [PEP 440](https://peps.python.org/pep-0440/) with SemVer meaning (pr
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.10.0] - 2026-10-30
10
+
11
+ Fixes a password leak in nested values and detects more credentials and identifiers. A run that
12
+ passed before can now redact, review or block, so re-check your policies before upgrading a gate.
13
+
14
+ ### Added
15
+
16
+ - `ssn_pattern` detector for dashed US Social Security numbers (`pii.government_id`). They were
17
+ labelled `pii.phone`, so government-ID rules did not apply and a strict template can now block.
18
+
19
+ ### Fixed
20
+
21
+ - `secret_pattern` no longer stops at `{` for nested values such as Actuator dumps, which left
22
+ the real value in the output.
23
+ - `secret_pattern` now finds camelCase password names, `Cookie` and `Set-Cookie` headers, and
24
+ session and CSRF cookie values. `luhn_card` finds labelled CVV and CVC values.
25
+ - Fewer false alarms: shell `PWD` paths, code such as `settings.X` or `request.Y` on the right of
26
+ an assignment, and an HTTP status followed by a byte count.
27
+
9
28
  ## [0.9.0] - 2026-10-30
10
29
 
11
30
  Detects more values. A run that passed before can now redact, review or block, so re-check your
@@ -296,7 +315,8 @@ Pre-release (alpha). Licensed under Apache-2.0. Not a final release: the securit
296
315
  See `docs/limitations.md`. Deferred: optional NER/Presidio detectors, stdin/stdout streaming,
297
316
  directory processing, structured log parsers, key files, reversible token vault, lossy decoding.
298
317
 
299
- [Unreleased]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.9.0...HEAD
318
+ [Unreleased]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.10.0...HEAD
319
+ [0.10.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.9.0...v0.10.0
300
320
  [0.9.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.8.0...v0.9.0
301
321
  [0.8.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.7.1...v0.8.0
302
322
  [0.7.1]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.7.0...v0.7.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ciphergate-cli
3
- Version: 0.9.0
3
+ Version: 0.10.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.9.0 (alpha).** No detector finds every sensitive value. A passing run means the
53
+ > **Status: 0.10.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.
@@ -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.9.0 (alpha).** No detector finds every sensitive value. A passing run means the
14
+ > **Status: 0.10.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.
@@ -12,11 +12,12 @@ are engineering estimates mapped to bands (`low` < 0.5 <= `medium` < 0.8 <= `hig
12
12
  | `email_regex` | `pii.email` | `alice.test@example.com`, `alice.test%40example.com` | `a..b@example.com`, `user@localhost` | 0.9 | ASCII only; no IDN; obfuscated forms (`alice at example dot com`) missed; `%40` is the only percent-encoding recognised outside `accesslog` |
13
13
  | `phone_pattern` | `pii.phone` | `+1 202 555 0143` (high), `(202) 555-0143` (medium) | dates, IPs, card numbers, repeated digits, < 7 digits | 0.3 bare digits, 0.55–0.8 prefixed/formatted, +0.2 keyword | no numbering-plan validation; IDs can look like phones (low band) |
14
14
  | `ip_address` | `pii.ip_address` (global), `network.telemetry` (private/loopback/link-local) | `8.8.8.8`, `10.0.0.1`, `2606:4700::1111`, `fe80::1%eth0` | `256.1.1.1`, `std::vector`, `12:30:45` | 0.85 / 0.8 | version strings like `1.2.3.4` match; documentation ranges flagged with evidence |
15
- | `luhn_card` | `pii.payment_card` | `4111 1111 1111 1111` | Luhn-invalid, mixed separators, repeated digits | 0.4 Luhn-only, 0.8 issuer prefix, +0.15 keyword | random IDs pass Luhn ~10% (low band); cards split across fields missed |
15
+ | `luhn_card` | `pii.payment_card` | `4111 1111 1111 1111` | Luhn-invalid, mixed separators, repeated digits | 0.4 Luhn-only, 0.8 issuer prefix, +0.15 keyword; labelled CVV/CVC values 0.85 | random IDs pass Luhn ~10% (low band); cards split across fields missed |
16
16
  | `mac_address` | `pii.device_identifier` | `00:1A:2B:3C:4D:5E`, `00-1a-2b-3c-4d-5e`, `001a.2b3c.4d5e` | all-zero, broadcast, mixed separators, `12:30:45` | 0.85, 0.6 when all digits | bare 12-hex values are missed; digit-only IDs in this shape are flagged |
17
17
  | `iban` | `pii.bank_account` | a mod-97-valid IBAN, compact or space-grouped | wrong check digits, lowercase | 0.9 | country length and national format are not checked; about 1 in 97 random strings of this shape pass; lowercase forms are missed |
18
+ | `ssn_pattern` | `pii.government_id` | `219-09-9999` | never-issued areas (`000`, `666`, `9xx`), group `00`, serial `0000`, undashed | 0.8, 0.95 with a keyword | US only; undashed numbers are missed; other dashed IDs in this shape are flagged |
18
19
  | `date_pattern` | `pii.date_of_birth` with field/keyword evidence; `date.generic` if enabled | `dob: 1990-04-12`, field `birth_date` | `created 2026-01-05` (no DOB evidence), invalid dates | 0.85 / 0.6 | `DD/MM` vs `MM/DD` ambiguity flagged; free-text DOBs without keywords missed |
19
- | `secret_pattern` | `credential.secret` | PEM blocks, `Bearer ...`, `AKIA...`, `ghp_...`, `xox?-`, `sk_test_...`, `AIza...`, JWTs, `scheme://u:pw@`, `Password=...;`, `password=`, assignments to `PGPASSWORD` or `MYSQL_PWD`, `api_key:`, `token=` | `password=`, `password: null`, markers, `******` | 0.8–0.99 | base64/hex-wrapped, split, custom-format or high-entropy secrets without keywords are **missed**; prose like `password: required` is flagged |
20
+ | `secret_pattern` | `credential.secret` | PEM blocks, `Bearer ...`, `AKIA...`, `ghp_...`, `xox?-`, `sk_test_...`, `AIza...`, JWTs, `scheme://u:pw@`, `Password=...;`, `password=`, assignments to `PGPASSWORD`, `MYSQL_PWD` or camelCase `...Password` names, `Cookie` and `Set-Cookie` headers, session and CSRF cookie values, `api_key:`, `token=` | `password=`, `password: null`, markers, `******` | 0.8–0.99 | base64/hex-wrapped, split, custom-format or high-entropy secrets without keywords are **missed**; prose like `password: required` is flagged |
20
21
  | `internal_host` | `network.telemetry` | `api-1.prod.internal`, `db.corp`, `localhost` | `example.com`, `a.internalx` | 0.75 | internal names without listed suffixes missed; configurable |
21
22
  | `custom:<id>` | configured | | | configured | regex-safety checks are heuristics |
22
23
  | `presidio_ner` (optional, off by default) | Presidio entities -> mapped categories (default `PERSON` -> `pii.person_name`, `LOCATION` -> `location.coarse`) | `ticket opened by <a person's name>` | numbers, single characters, CipherGate markers/pseudonyms | Presidio score, filtered by `min_score` | statistical model: misses and false positives, language/model dependent, slow; requires `ciphergate-cli[ner]` plus a locally installed spaCy model |
@@ -52,7 +52,7 @@ A pinned value combined with a different caller value fails with
52
52
  ## Detectors
53
53
 
54
54
  `disabled` (built-in IDs: `field_name`, `secret_pattern`, `luhn_card`, `ip_address`,
55
- `date_pattern`, `email_regex`, `internal_host`, `phone_pattern`, `mac_address`, `iban`),
55
+ `date_pattern`, `email_regex`, `internal_host`, `phone_pattern`, `mac_address`, `iban`, `ssn_pattern`),
56
56
  `dates_emit_generic` (report
57
57
  non-DOB dates as `date.generic`), `internal_host_suffixes` (default `.internal .local .corp .lan
58
58
  .intranet .localdomain`), `custom` (list of `{id, category, pattern, confidence, ignore_case}`;
@@ -110,6 +110,8 @@ ignore = [
110
110
  [tool.ruff.lint.per-file-ignores]
111
111
  "tests/**" = ["S101", "S105", "S106", "S108", "S311", "PLC0415", "S607"]
112
112
  "scripts/**" = ["S607", "T201"]
113
+ # Reproducible synthetic data, and optional tools that may not be installed.
114
+ "scripts/benchmark/**" = ["S311", "PLC0415", "S607", "T201"]
113
115
 
114
116
  [tool.mypy]
115
117
  python_version = "3.11"
@@ -0,0 +1,39 @@
1
+ # Detection benchmark
2
+
3
+ Measures what CipherGate catches and flags wrongly on generated, labelled log lines, next to other
4
+ local tools. It is an engineering aid for finding gaps. It is not part of the package, CI or the
5
+ test suite, and it never contacts a service.
6
+
7
+ ## Do not use the results as a public claim
8
+
9
+ - The corpus is written by the people who wrote the detectors, so it leans toward CipherGate.
10
+ - The data is synthetic. Real logs behave differently.
11
+ - Other tools run on default settings, and their results depend on versions and models.
12
+ - A high score here does not show that sensitive data is gone from any file.
13
+
14
+ ## Run it
15
+
16
+ Use a separate virtual environment, because these tools are not project dependencies:
17
+
18
+ ```bash
19
+ python -m venv /path/outside/repo/venv
20
+ # Activate it, then:
21
+ python -m pip install . detect-secrets scrubadub presidio-analyzer
22
+ python -m pip install en_core_web_sm # see the spaCy docs for the model install command
23
+ python scripts/benchmark/compare_tools.py --out /path/outside/repo/results
24
+ ```
25
+
26
+ Only CipherGate is required. Any other tool that is not installed is skipped and listed under
27
+ `not run`. To include gitleaks, install it yourself and pass `--gitleaks PATH`.
28
+
29
+ `--out` must be outside the repository, because `corpus.jsonl` holds format-shaped fake
30
+ credentials that must not be committed. The tool refuses a path inside the repository. Results
31
+ are repeatable for the same `--seed` and tool versions.
32
+
33
+ ## Reading the output
34
+
35
+ Each row is a data class, and each cell is caught/total. A positive counts only if the finding
36
+ overlaps the planted value; for CipherGate, the value must be gone from the output. `NEGATIVE`
37
+ counts false alarms on clean lines. `presidio@0.5` is Presidio with a 0.5 score threshold.
38
+
39
+ Random 16-digit IDs pass the card checksum about 10% of the time, so `NEGATIVE` varies by seed.
@@ -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.9.0"
27
+ __version__ = "0.10.0"
28
28
 
29
29
  from ciphergate.config import RuntimeSettings
30
30
  from ciphergate.constants import (
@@ -12,6 +12,9 @@ from ciphergate.detectors.base import make_finding, preceding_context
12
12
  from ciphergate.models.finding import Finding, ScanItem
13
13
 
14
14
  _CARD_RE = re.compile(r"(?<![0-9A-Za-z_])(\d(?:[ -]?\d){12,18})(?![0-9A-Za-z_])")
15
+ _CVV_RE = re.compile(
16
+ r"(?i)(?<![a-z0-9])(?:cvv2?|cvc2?)(?:\\?[\"'])?\s{0,3}[:=]\s{0,3}(?:\\?[\"'])?(\d{3,4})(?!\d)"
17
+ )
15
18
  _KEYWORDS = ("card", "visa", "mastercard", "amex", "pan", "cc", "credit", "debit", "payment")
16
19
 
17
20
 
@@ -45,9 +48,21 @@ class PaymentCardDetector:
45
48
 
46
49
  def detect(self, item: ScanItem) -> list[Finding]:
47
50
  text = item.text
48
- if not text or sum(c.isdigit() for c in text) < 13:
51
+ if not text:
49
52
  return []
50
- out: list[Finding] = []
53
+ out = [
54
+ make_finding(
55
+ item,
56
+ category="pii.payment_card",
57
+ detector=self.id,
58
+ confidence=0.85,
59
+ evidence=("pattern_match", "context_keyword"),
60
+ span=(m.start(1), m.end(1)),
61
+ )
62
+ for m in _CVV_RE.finditer(text)
63
+ ]
64
+ if sum(c.isdigit() for c in text) < 13:
65
+ return out
51
66
  for m in _CARD_RE.finditer(text):
52
67
  raw = m.group(1)
53
68
  digits = raw.replace(" ", "").replace("-", "")
@@ -21,6 +21,7 @@ _PHONE_RE = re.compile(
21
21
  )
22
22
  _DATE_LIKE = re.compile(r"^\d{4}[-./]\d{1,2}[-./]\d{1,2}$|^\d{1,2}[-./]\d{1,2}[-./]\d{2,4}$")
23
23
  _SEPARATED = re.compile(r"^\(?\d{2,4}\)?[ .-]\d{2,5}[ .-]\d{2,5}(?:[ .-]\d{2,5})?$")
24
+ _HTTP_STATUS_BYTES = re.compile(r"^[1-5]\d{2} \d{1,12}$")
24
25
  _KEYWORDS = ("phone", "tel", "mobile", "cell", "call", "fax", "sms", "whatsapp", "contact")
25
26
 
26
27
 
@@ -40,6 +41,9 @@ class PhoneDetector:
40
41
  continue
41
42
  if _DATE_LIKE.match(core.strip()):
42
43
  continue
44
+ context = preceding_context(text, m.start(), _KEYWORDS)
45
+ if _HTTP_STATUS_BYTES.match(core.strip()) and not context:
46
+ continue
43
47
  evidence = ["pattern_match"]
44
48
  if core.startswith("+"):
45
49
  confidence = 0.8 if len(digits) >= 8 else 0.55
@@ -49,7 +53,7 @@ class PhoneDetector:
49
53
  evidence.append("separator_layout")
50
54
  else:
51
55
  confidence = 0.3
52
- if preceding_context(text, m.start(), _KEYWORDS):
56
+ if context:
53
57
  confidence += 0.2
54
58
  evidence.append("context_keyword")
55
59
  if "x" in candidate.lower() or "ext" in candidate.lower():
@@ -16,6 +16,7 @@ from ciphergate.detectors.network import InternalHostDetector
16
16
  from ciphergate.detectors.payment_card import PaymentCardDetector
17
17
  from ciphergate.detectors.phone import PhoneDetector
18
18
  from ciphergate.detectors.secrets import PEM_BEGIN_RE, PEM_END_RE, SecretDetector
19
+ from ciphergate.detectors.ssn import SsnDetector
19
20
  from ciphergate.models.finding import Finding, ScanItem
20
21
  from ciphergate.models.policy import DetectorSettings, PolicyDocument
21
22
 
@@ -30,11 +31,20 @@ BUILTIN_DETECTOR_IDS: tuple[str, ...] = (
30
31
  "phone_pattern",
31
32
  "mac_address",
32
33
  "iban",
34
+ "ssn_pattern",
33
35
  )
34
36
 
35
37
  # Phone matching is the least specific pattern; drop phone spans that overlap these detectors.
36
38
  _PHONE_SUPPRESSORS = frozenset(
37
- {"luhn_card", "ip_address", "date_pattern", "secret_pattern", "mac_address", "iban"}
39
+ {
40
+ "luhn_card",
41
+ "ip_address",
42
+ "date_pattern",
43
+ "secret_pattern",
44
+ "mac_address",
45
+ "iban",
46
+ "ssn_pattern",
47
+ }
38
48
  )
39
49
 
40
50
 
@@ -67,6 +77,7 @@ class DetectorSet:
67
77
  "phone_pattern": PhoneDetector(),
68
78
  "mac_address": MacAddressDetector(),
69
79
  "iban": IbanDetector(),
80
+ "ssn_pattern": SsnDetector(),
70
81
  }
71
82
  for detector_id in BUILTIN_DETECTOR_IDS:
72
83
  if detector_id not in disabled:
@@ -185,6 +196,8 @@ def unsafe_replacement_reason(value: str) -> str | None:
185
196
  return "looks like a real IBAN"
186
197
  if MacAddressDetector().detect(probe):
187
198
  return "looks like a real MAC address"
199
+ if SsnDetector().detect(probe):
200
+ return "looks like a real government identifier"
188
201
  for f in EmailDetector().detect(probe):
189
202
  if "reserved_domain" not in f.evidence:
190
203
  return "email replacements must use a reserved domain such as example.invalid"
@@ -69,17 +69,68 @@ _PATTERNS: tuple[_Pattern, ...] = (
69
69
  r"api[_-]?key|apikey|access[_-]?token|"
70
70
  r"refresh[_-]?token|auth[_-]?token|client[_-]?secret|secret[_-]?key|private[_-]?key|"
71
71
  r"access[_-]?key|token|credentials?)"
72
+ r"(?:\\?[\"'])?\s{0,3}[:=]\s{0,3}"
73
+ # Spring Actuator and similar dumps nest the value in an object.
74
+ r"(?:\{\s{0,3}\\?[\"']value\\?[\"']\s{0,3}:\s{0,3})?(?:\\?[\"'])?"
75
+ r"([^\s\"'\\,;&}]{1,512})"
76
+ ),
77
+ 1,
78
+ 0.8,
79
+ ),
80
+ _Pattern(
81
+ "camel_case_password",
82
+ re.compile(
83
+ r"(?<![A-Za-z0-9])[a-z][A-Za-z0-9]{0,40}Password"
72
84
  r"(?:\\?[\"'])?\s{0,3}[:=]\s{0,3}(?:\\?[\"'])?"
73
85
  r"([^\s\"'\\,;&}]{1,512})"
74
86
  ),
75
87
  1,
76
88
  0.8,
77
89
  ),
90
+ _Pattern(
91
+ "cookie_header",
92
+ re.compile(
93
+ r"(?i)(?<![a-z0-9_-])(?:set-)?cookie(?:\\?[\"'])?\s{0,3}:\s{0,3}(?:\\?[\"'])?"
94
+ r"([^\s=;\"']{1,128}=[^\r\n\"']{1,2048})"
95
+ ),
96
+ 1,
97
+ 0.85,
98
+ ),
99
+ _Pattern(
100
+ "session_cookie",
101
+ re.compile(
102
+ r"(?i)(?<![a-z0-9_-])(?:(?:sessionid|jsessionid|phpsessid|connect\.sid|csrftoken|"
103
+ r"csrfmiddlewaretoken|xsrf-token)(?:\\?[\"'])?\s{0,3}[:=]\s{0,3}|session=)"
104
+ r"(?:\\?[\"'])?([^\s\"'\\,;&}]{8,512})"
105
+ ),
106
+ 1,
107
+ 0.8,
108
+ ),
78
109
  )
79
110
 
80
111
  _PLACEHOLDER_VALUES = frozenset(
81
- {"", "null", "none", "nil", "undefined", "true", "false", "redacted", "***", "******", "xxx"}
112
+ {
113
+ "",
114
+ "null",
115
+ "none",
116
+ "nil",
117
+ "undefined",
118
+ "true",
119
+ "false",
120
+ "redacted",
121
+ "***",
122
+ "******",
123
+ "xxx",
124
+ "{",
125
+ "[",
126
+ }
82
127
  )
128
+ # Code that reads a secret, not the secret. Only well-known names, because a dotted value can
129
+ # itself be a password.
130
+ _CODE_REFERENCE_RE = re.compile(
131
+ r"(?:settings|config|request|self|this|args|params|env|os\.environ|process\.env)[.\[]"
132
+ )
133
+ _PATH_VALUE_RE = re.compile(r"(?:/|~/|[A-Za-z]:\\)")
83
134
 
84
135
 
85
136
  def _is_placeholder(value: str) -> bool:
@@ -110,7 +161,7 @@ class SecretDetector:
110
161
  for m in pattern.regex.finditer(text):
111
162
  start, end = m.start(pattern.group), m.end(pattern.group)
112
163
  value = m.group(pattern.group)
113
- if _is_placeholder(value):
164
+ if _is_placeholder(value) or self._is_reference(pattern.name, m, value):
114
165
  continue
115
166
  if any(s <= start and end <= e for s, e in covered):
116
167
  continue
@@ -118,6 +169,15 @@ class SecretDetector:
118
169
  out.append(self._finding(item, (start, end), pattern.name, pattern.confidence))
119
170
  return out
120
171
 
172
+ @staticmethod
173
+ def _is_reference(family: str, match: re.Match[str], value: str) -> bool:
174
+ if family not in ("assignment", "connection_string_password"):
175
+ return False
176
+ if _CODE_REFERENCE_RE.match(value):
177
+ return True
178
+ # PWD is also the shell's working directory.
179
+ return match.group(0)[:3].lower() == "pwd" and bool(_PATH_VALUE_RE.match(value))
180
+
121
181
  def _finding(
122
182
  self, item: ScanItem, span: tuple[int, int], family: str, confidence: float
123
183
  ) -> Finding:
@@ -0,0 +1,45 @@
1
+ """US Social Security number detector: dashed form, with the never-issued ranges excluded.
2
+
3
+ Bare nine-digit runs are not matched because they are indistinguishable from other identifiers.
4
+ Other countries' identifiers are not covered.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+ from ciphergate.detectors.base import make_finding, preceding_context
12
+ from ciphergate.models.finding import Finding, ScanItem
13
+
14
+ _SSN_RE = re.compile(r"(?<![\d-])(\d{3})-(\d{2})-(\d{4})(?![\d-])")
15
+ _KEYWORDS = ("ssn", "social", "tax", "tin")
16
+
17
+
18
+ class SsnDetector:
19
+ id = "ssn_pattern"
20
+
21
+ def detect(self, item: ScanItem) -> list[Finding]:
22
+ text = item.text
23
+ if not text or len(text) < 11:
24
+ return []
25
+ out: list[Finding] = []
26
+ for m in _SSN_RE.finditer(text):
27
+ area, group, serial = m.groups()
28
+ if area in ("000", "666") or area[0] == "9" or group == "00" or serial == "0000":
29
+ continue
30
+ evidence = ["pattern_match"]
31
+ confidence = 0.8
32
+ if preceding_context(text, m.start(), _KEYWORDS):
33
+ confidence = 0.95
34
+ evidence.append("context_keyword")
35
+ out.append(
36
+ make_finding(
37
+ item,
38
+ category="pii.government_id",
39
+ detector=self.id,
40
+ confidence=confidence,
41
+ evidence=tuple(evidence),
42
+ span=(m.start(), m.end()),
43
+ )
44
+ )
45
+ return out
@@ -0,0 +1,76 @@
1
+ """The detection benchmark: repeatable corpus, valid labels, results kept out of the repository."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from ciphergate import load_policy
12
+ from ciphergate.detectors.iban import iban_valid
13
+ from ciphergate.detectors.payment_card import luhn_valid
14
+ from tests.conftest import ROOT
15
+
16
+ sys.path.insert(0, str(ROOT / "scripts" / "benchmark"))
17
+
18
+ import compare_tools
19
+
20
+
21
+ def test_corpus_is_repeatable_for_a_seed() -> None:
22
+ assert compare_tools.Corpus(1).build(3) == compare_tools.Corpus(1).build(3)
23
+ assert compare_tools.Corpus(1).build(3) != compare_tools.Corpus(2).build(3)
24
+
25
+
26
+ def test_every_class_has_templates_and_labels_are_present_in_the_text() -> None:
27
+ corpus = compare_tools.Corpus(1)
28
+ assert set(corpus.generators) == set(compare_tools.TEMPLATES)
29
+ for case in corpus.build(4):
30
+ assert case["value"] is None or case["value"] in case["text"]
31
+
32
+
33
+ def test_negatives_carry_no_label() -> None:
34
+ negatives = [c for c in compare_tools.Corpus(1).build(2) if c["cls"] == "NEGATIVE"]
35
+ assert len(negatives) == 2 * compare_tools.NEGATIVE_FACTOR
36
+ assert all(c["value"] is None for c in negatives)
37
+
38
+
39
+ def test_generated_values_pass_their_own_checks() -> None:
40
+ corpus = compare_tools.Corpus(1)
41
+ for _ in range(50):
42
+ assert iban_valid(corpus.iban())
43
+ assert luhn_valid(corpus.luhn("4"))
44
+
45
+
46
+ def test_benchmark_policy_is_valid() -> None:
47
+ assert load_policy(compare_tools.POLICY).name == "benchmark-redact-everything"
48
+
49
+
50
+ def test_ciphergate_scoring_on_a_small_corpus(tmp_path: Path) -> None:
51
+ cases = compare_tools.Corpus(1).build(1)
52
+ hits, _ = compare_tools.run_ciphergate(cases, tmp_path)
53
+ scored = {c["cls"]: hit for c, hit in zip(cases, hits, strict=True) if c["value"]}
54
+ assert scored["email"] and scored["iban"] and scored["private_key_pem"]
55
+ assert not scored["person_name"] # a known gap without NER; the score must be able to say so
56
+
57
+
58
+ @pytest.mark.parametrize("inside", [ROOT, ROOT / "build" / "bench"])
59
+ def test_refuses_to_write_inside_the_repository(inside: Path) -> None:
60
+ assert compare_tools.is_inside_repo(inside)
61
+ assert compare_tools.main(["--out", str(inside), "--per-class", "1"]) == 2
62
+
63
+
64
+ def test_output_outside_the_repository_is_allowed(tmp_path: Path) -> None:
65
+ assert not compare_tools.is_inside_repo(tmp_path)
66
+
67
+
68
+ def test_results_are_not_tracked() -> None:
69
+ tracked = subprocess.run(
70
+ ["git", "ls-files", "scripts/benchmark"],
71
+ cwd=ROOT,
72
+ capture_output=True,
73
+ text=True,
74
+ check=True,
75
+ ).stdout.split()
76
+ assert not [f for f in tracked if f.endswith((".jsonl", ".json"))]
@@ -22,6 +22,7 @@ from ciphergate.detectors.phone import PhoneDetector
22
22
  from ciphergate.detectors.regex_safety import regex_problems
23
23
  from ciphergate.detectors.registry import DetectorSet, TextBlockState, unsafe_replacement_reason
24
24
  from ciphergate.detectors.secrets import SecretDetector
25
+ from ciphergate.detectors.ssn import SsnDetector
25
26
  from ciphergate.models.finding import ScanItem
26
27
  from ciphergate.models.policy import Classification, CustomDetector, DetectorSettings
27
28
 
@@ -493,3 +494,141 @@ def test_env_style_password_names(name: str) -> None:
493
494
  @pytest.mark.parametrize("name", ["nopassword", "haspassword", "password_reset_enabled"])
494
495
  def test_other_password_words_stay_unmatched(name: str) -> None:
495
496
  assert not SecretDetector().detect(item(f"{name}=enabled"))
497
+
498
+
499
+ # ------------------------------------------------- bugs found by simulated developer runs
500
+
501
+
502
+ TEST_ONLY = "TEST_ONLY_NOT_A_CREDENTIAL"
503
+
504
+
505
+ @pytest.mark.parametrize(
506
+ ("text", "expected"),
507
+ [
508
+ ("ssn 219-09-9999 failed", ["219-09-9999"]),
509
+ ("078-05-1120", ["078-05-1120"]),
510
+ ("area 000-12-3456", []),
511
+ ("area 666-12-3456", []),
512
+ ("area 912-12-3456", []),
513
+ ("group 219-00-9999", []),
514
+ ("serial 219-09-0000", []),
515
+ ("long 1219-09-99991", []),
516
+ ("undashed 219099999", []),
517
+ ],
518
+ )
519
+ def test_ssn(text: str, expected: list[str]) -> None:
520
+ findings = SsnDetector().detect(item(text))
521
+ assert spans(findings, text) == expected
522
+ assert all(f.category == "pii.government_id" for f in findings)
523
+
524
+
525
+ def test_ssn_is_not_reported_as_a_phone_number() -> None:
526
+ categories = {f.category for f in DetectorSet(DetectorSettings()).scan(item("id 219-09-9999"))}
527
+ assert categories == {"pii.government_id"}
528
+
529
+
530
+ @pytest.mark.parametrize(
531
+ "text",
532
+ [
533
+ f'{{"spring.datasource.password":{{"value":"{TEST_ONLY}"}}}}',
534
+ f'{{"password": {{ "value": "{TEST_ONLY}" }}}}',
535
+ ],
536
+ )
537
+ def test_nested_value_is_found_not_the_brace(text: str) -> None:
538
+ assert spans(SecretDetector().detect(item(text)), text) == [TEST_ONLY]
539
+
540
+
541
+ def test_lone_brace_is_not_a_secret() -> None:
542
+ assert not SecretDetector().detect(item('{"password":{"rotated":true}}'))
543
+
544
+
545
+ @pytest.mark.parametrize(
546
+ "text",
547
+ [
548
+ f"-Djavax.net.ssl.keyStorePassword={TEST_ONLY}",
549
+ f"trustStorePassword: {TEST_ONLY}",
550
+ f'{{"dbPassword":"{TEST_ONLY}"}}',
551
+ ],
552
+ )
553
+ def test_camel_case_password_names(text: str) -> None:
554
+ assert spans(SecretDetector().detect(item(text)), text) == [TEST_ONLY]
555
+
556
+
557
+ @pytest.mark.parametrize(
558
+ ("text", "expected"),
559
+ [
560
+ (
561
+ f"Cookie: sessionid={TEST_ONLY}; csrftoken={TEST_ONLY}",
562
+ f"sessionid={TEST_ONLY}; csrftoken={TEST_ONLY}",
563
+ ),
564
+ (f"Set-Cookie: sid={TEST_ONLY}; Path=/", f"sid={TEST_ONLY}; Path=/"),
565
+ (f"'sessionid': '{TEST_ONLY}'", TEST_ONLY),
566
+ (f"csrfmiddlewaretoken={TEST_ONLY}&next=/", TEST_ONLY),
567
+ (f"JSESSIONID={TEST_ONLY}", TEST_ONLY),
568
+ (f"session={TEST_ONLY}", TEST_ONLY),
569
+ ],
570
+ )
571
+ def test_cookies_and_session_values(text: str, expected: str) -> None:
572
+ assert spans(SecretDetector().detect(item(text)), text) == [expected]
573
+
574
+
575
+ @pytest.mark.parametrize(
576
+ "text",
577
+ ["cookie: required", "session: established", "session=short", "sessionid=abc"],
578
+ )
579
+ def test_cookie_prose_is_not_a_secret(text: str) -> None:
580
+ assert not SecretDetector().detect(item(text))
581
+
582
+
583
+ @pytest.mark.parametrize(
584
+ "text",
585
+ [
586
+ "PWD=/home/heidi",
587
+ "pwd=~/project",
588
+ "PWD=C:\\Users\\heidi",
589
+ 'token=request.POST["card_token"]',
590
+ "api_key=settings.CRM_KEY",
591
+ "secret=os.environ['SIGNING']",
592
+ "password=config.db_password",
593
+ ],
594
+ )
595
+ def test_paths_and_code_references_are_not_secrets(text: str) -> None:
596
+ assert not SecretDetector().detect(item(text))
597
+
598
+
599
+ @pytest.mark.parametrize(
600
+ ("text", "expected"),
601
+ [
602
+ (f"Password={TEST_ONLY};Server=db", TEST_ONLY),
603
+ (f"PWD={TEST_ONLY}", TEST_ONLY),
604
+ ("password=/TEST_ONLY_slash", "/TEST_ONLY_slash"),
605
+ ("api_key=settingsTEST_ONLY", "settingsTEST_ONLY"),
606
+ ],
607
+ )
608
+ def test_real_values_near_those_exceptions_stay_found(text: str, expected: str) -> None:
609
+ assert spans(SecretDetector().detect(item(text)), text) == [expected]
610
+
611
+
612
+ @pytest.mark.parametrize(
613
+ ("text", "expected"),
614
+ [
615
+ ("cvv=123", ["123"]),
616
+ ('{"cvc": "1234"}', ["1234"]),
617
+ ("CVV2: 123", ["123"]),
618
+ ("cvv=12", []),
619
+ ("cvv=12345", []),
620
+ ("cvv=enabled", []),
621
+ ],
622
+ )
623
+ def test_labelled_cvv(text: str, expected: list[str]) -> None:
624
+ findings = PaymentCardDetector().detect(item(text))
625
+ assert spans(findings, text) == expected
626
+ assert all(f.category == "pii.payment_card" for f in findings)
627
+
628
+
629
+ def test_http_status_and_bytes_are_not_a_phone() -> None:
630
+ assert not PhoneDetector().detect(item('GET /x 200 1024 "-"'))
631
+
632
+
633
+ def test_a_number_after_a_phone_keyword_is_still_a_phone() -> None:
634
+ assert PhoneDetector().detect(item("phone 202 5550143"))
File without changes
File without changes