ciphergate-cli 0.8.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.
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/CHANGELOG.md +39 -1
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/PKG-INFO +2 -2
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/README.md +1 -1
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/docs/detector-coverage.md +6 -3
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/docs/policy-schema.md +2 -1
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/pyproject.toml +2 -0
- ciphergate_cli-0.10.0/scripts/benchmark/README.md +39 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/__init__.py +1 -1
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/email.py +8 -2
- ciphergate_cli-0.10.0/src/ciphergate/detectors/iban.py +53 -0
- ciphergate_cli-0.10.0/src/ciphergate/detectors/mac_address.py +47 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/payment_card.py +17 -2
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/phone.py +5 -1
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/registry.py +26 -1
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/secrets.py +64 -3
- ciphergate_cli-0.10.0/src/ciphergate/detectors/ssn.py +45 -0
- ciphergate_cli-0.10.0/tests/tooling/test_benchmark.py +76 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_accesslog.py +2 -1
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_detectors.py +242 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/.gitignore +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/LICENSE +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/NOTICE +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/SECURITY.md +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/docs/architecture.md +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/docs/claude-code-tooling.md +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/docs/examples.md +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/docs/limitations.md +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/docs/security-and-privacy.md +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/docs/threat-model.md +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/examples/expected/sample.dev_local_dev.csv +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/examples/expected/sample.dev_local_dev.json +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/examples/expected/sample.dev_local_dev.jsonl +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/examples/expected/sample.dev_local_dev.log +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/examples/sample.csv +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/examples/sample.json +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/examples/sample.jsonl +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/examples/sample.log +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/policies/dev_local.yml +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/policies/developer_logs.yml +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/policies/external_ai.yml +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/policies/financial_template.yml +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/policies/healthcare_template.yml +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/policies/local_debug.yml +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/policies/multi_environment.yml +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/policies/prd_external_ai.yml +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/policies/stg_test.yml +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/policies/test_dataset.yml +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/__main__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/cli.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/config.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/constants.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/base.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/custom_regex.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/dates.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/field_names.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/ip_address.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/network.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/presidio_ner.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/detectors/regex_safety.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/errors.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/models/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/models/context.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/models/finding.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/models/policy.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/models/report.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/atomic_output.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/batch.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/inspector.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/paths.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/planner.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/rescan.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/sanitizer.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/stdio.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/pipeline/streams.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/context.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/engine.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/loader.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/precedence.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/templates.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/policy/validator.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/py.typed +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/accesslog_reader.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/base.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/csv_reader.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/json_reader.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/jsonl_reader.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/jsonlog_reader.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/line_structure.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/logfmt_reader.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/stacktrace_reader.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/syslog_reader.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/readers/text_reader.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/reporting/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/reporting/audit.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/reporting/safe_text.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/reporting/serializer.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/reporting/terminal.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/templates/starter_policy.yml +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/date_shift.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/generalize.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/keys.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/pseudonymize.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/redact.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/registry.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/src/ciphergate/transforms/replace.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/conftest.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/integration/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/integration/test_api_batch_streams.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/integration/test_atomic_output.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/integration/test_batch.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/integration/test_cli_e2e.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/integration/test_domain_templates.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/integration/test_environment_matrix.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/integration/test_pipelines.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/integration/test_stdio.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/regression/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/regression/test_known_cases.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/security/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/security/test_key_file.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/security/test_no_network.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/security/test_no_raw_value_leaks.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/security/test_path_safety.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/security/test_policy_fail_closed.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/security/test_review_findings.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/tooling/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/tooling/test_ci_supply_chain.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/tooling/test_claude_tooling.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/tooling/test_git_tooling.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/tooling/test_publish_workflow.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/__init__.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_jsonlog.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_logfmt.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_policy_engine.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_policy_precedence.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_policy_templates.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_policy_validation.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_presidio_ner.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_properties.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_readers.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_stacktrace.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_syslog.py +0 -0
- {ciphergate_cli-0.8.0 → ciphergate_cli-0.10.0}/tests/unit/test_transforms.py +0 -0
|
@@ -6,6 +6,42 @@ 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
|
+
|
|
28
|
+
## [0.9.0] - 2026-10-30
|
|
29
|
+
|
|
30
|
+
Detects more values. A run that passed before can now redact, review or block, so re-check your
|
|
31
|
+
policies before upgrading a gate.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- `mac_address` detector (`pii.device_identifier`) and `iban` detector (`pii.bank_account`, mod-97
|
|
36
|
+
check). Both are on by default and can be turned off with `detectors.disabled`.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- `email_regex` also finds percent-encoded addresses (`%40`) in text, and `secret_pattern` finds
|
|
41
|
+
values assigned to `PGPASSWORD`, `MYSQL_PWD` and similar names (`DBPASSWORD`, `ROOTPASSWORD`).
|
|
42
|
+
These values now follow your policy rules, so a run can redact, review or block content it
|
|
43
|
+
used to pass.
|
|
44
|
+
|
|
9
45
|
## [0.8.0] - 2026-10-30
|
|
10
46
|
|
|
11
47
|
Adds the packaged policy templates to the Python API. Existing behaviour is unchanged.
|
|
@@ -279,7 +315,9 @@ Pre-release (alpha). Licensed under Apache-2.0. Not a final release: the securit
|
|
|
279
315
|
See `docs/limitations.md`. Deferred: optional NER/Presidio detectors, stdin/stdout streaming,
|
|
280
316
|
directory processing, structured log parsers, key files, reversible token vault, lossy decoding.
|
|
281
317
|
|
|
282
|
-
[Unreleased]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.
|
|
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
|
|
320
|
+
[0.9.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.8.0...v0.9.0
|
|
283
321
|
[0.8.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.7.1...v0.8.0
|
|
284
322
|
[0.7.1]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.7.0...v0.7.1
|
|
285
323
|
[0.7.0]: https://github.com/aashishkumar-tech/ciphergate/compare/v0.6.1...v0.7.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ciphergate-cli
|
|
3
|
-
Version: 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.
|
|
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.
|
|
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.
|
|
@@ -9,12 +9,15 @@ are engineering estimates mapped to bands (`low` < 0.5 <= `medium` < 0.8 <= `hig
|
|
|
9
9
|
|---|---|---|---|---|---|
|
|
10
10
|
| `field_name` | field names -> many categories | `email`, `contactEmail`, `api_key`, `date-of-birth`, `userId`, `trace_id` | `token_count`, `key_name`, `monkey`, `product_name`, `email_verified` | 0.45–0.85 per term | heuristic dictionary; benign values in sensitive-looking fields are flagged; sensitive data in generic fields (`notes`, `value`) is missed unless content detectors match |
|
|
11
11
|
| `policy_classification` | policy-declared fields | `admission_date` (healthcare template) | | configured | only as good as the policy |
|
|
12
|
-
| `email_regex` | `pii.email` | `alice.test@example.com` | `a..b@example.com`, `user@localhost` | 0.9 | ASCII only; no IDN; obfuscated forms (`alice at example dot com`) missed |
|
|
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
|
+
| `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
|
+
| `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 |
|
|
16
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 |
|
|
17
|
-
| `secret_pattern` | `credential.secret` | PEM blocks, `Bearer ...`, `AKIA...`, `ghp_...`, `xox?-`, `sk_test_...`, `AIza...`, JWTs, `scheme://u:pw@`, `Password=...;`, `password=`, `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 |
|
|
18
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 |
|
|
19
22
|
| `custom:<id>` | configured | | | configured | regex-safety checks are heuristics |
|
|
20
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,8 @@ 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`
|
|
55
|
+
`date_pattern`, `email_regex`, `internal_host`, `phone_pattern`, `mac_address`, `iban`, `ssn_pattern`),
|
|
56
|
+
`dates_emit_generic` (report
|
|
56
57
|
non-DOB dates as `date.generic`), `internal_host_suffixes` (default `.internal .local .corp .lan
|
|
57
58
|
.intranet .localdomain`), `custom` (list of `{id, category, pattern, confidence, ignore_case}`;
|
|
58
59
|
patterns are checked for compile errors, empty matches, nested quantifiers, backreferences and
|
|
@@ -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.
|
|
27
|
+
__version__ = "0.10.0"
|
|
28
28
|
|
|
29
29
|
from ciphergate.config import RuntimeSettings
|
|
30
30
|
from ciphergate.constants import (
|
|
@@ -12,6 +12,12 @@ _EMAIL_RE = re.compile(
|
|
|
12
12
|
r"([A-Za-z0-9._%+-]{1,64})@((?:[A-Za-z0-9-]{1,63}\.){1,8}[A-Za-z]{2,63})"
|
|
13
13
|
r"(?![A-Za-z0-9-])"
|
|
14
14
|
)
|
|
15
|
+
# Percent-encoded form in URLs and query strings: the "@" is written as %40.
|
|
16
|
+
_ENCODED_EMAIL_RE = re.compile(
|
|
17
|
+
r"(?<![A-Za-z0-9._%+-])"
|
|
18
|
+
r"([A-Za-z0-9._%+-]{1,64})%40((?:[A-Za-z0-9-]{1,63}\.){1,8}[A-Za-z]{2,63})"
|
|
19
|
+
r"(?![A-Za-z0-9-])"
|
|
20
|
+
)
|
|
15
21
|
|
|
16
22
|
RESERVED_EMAIL_DOMAINS = ("example.com", "example.org", "example.net")
|
|
17
23
|
RESERVED_EMAIL_TLDS = (".invalid", ".example", ".test", ".localhost")
|
|
@@ -31,10 +37,10 @@ class EmailDetector:
|
|
|
31
37
|
|
|
32
38
|
def detect(self, item: ScanItem) -> list[Finding]:
|
|
33
39
|
text = item.text
|
|
34
|
-
if not text or "@" not in text:
|
|
40
|
+
if not text or ("@" not in text and "%40" not in text):
|
|
35
41
|
return []
|
|
36
42
|
out: list[Finding] = []
|
|
37
|
-
for m in _EMAIL_RE.finditer(text):
|
|
43
|
+
for m in (*_EMAIL_RE.finditer(text), *_ENCODED_EMAIL_RE.finditer(text)):
|
|
38
44
|
local, domain = m.group(1), m.group(2)
|
|
39
45
|
if local.startswith(".") or local.endswith(".") or ".." in local or ".." in domain:
|
|
40
46
|
continue
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""IBAN detector: compact or space-grouped, uppercase, validated with the ISO 7064 mod-97 check.
|
|
2
|
+
|
|
3
|
+
Country-specific lengths and national formats are not checked. Lowercase forms are not matched.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from ciphergate.detectors.base import make_finding
|
|
11
|
+
from ciphergate.models.finding import Finding, ScanItem
|
|
12
|
+
|
|
13
|
+
_IBAN_RE = re.compile(
|
|
14
|
+
r"(?<![0-9A-Za-z])"
|
|
15
|
+
r"([A-Z]{2}\d{2}(?:[A-Z0-9]{11,30}|(?: [A-Z0-9]{4}){2,7}(?: [A-Z0-9]{1,3})?))"
|
|
16
|
+
r"(?![0-9A-Za-z])"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def iban_valid(compact: str) -> bool:
|
|
21
|
+
if not 15 <= len(compact) <= 34:
|
|
22
|
+
return False
|
|
23
|
+
rearranged = compact[4:] + compact[:4]
|
|
24
|
+
return int("".join(str(int(c, 36)) for c in rearranged)) % 97 == 1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class IbanDetector:
|
|
28
|
+
id = "iban"
|
|
29
|
+
|
|
30
|
+
def detect(self, item: ScanItem) -> list[Finding]:
|
|
31
|
+
text = item.text
|
|
32
|
+
if not text or len(text) < 15:
|
|
33
|
+
return []
|
|
34
|
+
out: list[Finding] = []
|
|
35
|
+
for m in _IBAN_RE.finditer(text):
|
|
36
|
+
# A following uppercase word can join a spaced match, so retry without trailing groups.
|
|
37
|
+
groups = m.group(1).split(" ")
|
|
38
|
+
while groups:
|
|
39
|
+
if iban_valid("".join(groups)):
|
|
40
|
+
end = m.start(1) + len(" ".join(groups))
|
|
41
|
+
out.append(
|
|
42
|
+
make_finding(
|
|
43
|
+
item,
|
|
44
|
+
category="pii.bank_account",
|
|
45
|
+
detector=self.id,
|
|
46
|
+
confidence=0.9,
|
|
47
|
+
evidence=("pattern_match", "checksum_valid"),
|
|
48
|
+
span=(m.start(1), end),
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
break
|
|
52
|
+
groups.pop()
|
|
53
|
+
return out
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""MAC address detector: six hex pairs with one separator, or Cisco dotted form.
|
|
2
|
+
|
|
3
|
+
All-digit forms are medium confidence because they can be other identifiers. The all-zero and
|
|
4
|
+
broadcast addresses identify no device and are skipped.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from ciphergate.detectors.base import make_finding
|
|
12
|
+
from ciphergate.models.finding import Finding, ScanItem
|
|
13
|
+
|
|
14
|
+
_MAC_RE = re.compile(
|
|
15
|
+
r"(?<![0-9A-Fa-f:.-])"
|
|
16
|
+
r"([0-9A-Fa-f]{2}(?:[:-][0-9A-Fa-f]{2}){5}|[0-9A-Fa-f]{4}(?:\.[0-9A-Fa-f]{4}){2})"
|
|
17
|
+
r"(?![0-9A-Za-z:.-])"
|
|
18
|
+
)
|
|
19
|
+
_NON_DEVICE = frozenset({"0" * 12, "f" * 12})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MacAddressDetector:
|
|
23
|
+
id = "mac_address"
|
|
24
|
+
|
|
25
|
+
def detect(self, item: ScanItem) -> list[Finding]:
|
|
26
|
+
text = item.text
|
|
27
|
+
if not text or len(text) < 14:
|
|
28
|
+
return []
|
|
29
|
+
out: list[Finding] = []
|
|
30
|
+
for m in _MAC_RE.finditer(text):
|
|
31
|
+
raw = m.group(1)
|
|
32
|
+
if len({c for c in raw if c in ":-."}) > 1:
|
|
33
|
+
continue
|
|
34
|
+
compact = re.sub(r"[:.-]", "", raw).lower()
|
|
35
|
+
if compact in _NON_DEVICE:
|
|
36
|
+
continue
|
|
37
|
+
out.append(
|
|
38
|
+
make_finding(
|
|
39
|
+
item,
|
|
40
|
+
category="pii.device_identifier",
|
|
41
|
+
detector=self.id,
|
|
42
|
+
confidence=0.85 if not compact.isdigit() else 0.6,
|
|
43
|
+
evidence=("pattern_match",),
|
|
44
|
+
span=(m.start(1), m.end(1)),
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return out
|
|
@@ -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
|
|
51
|
+
if not text:
|
|
49
52
|
return []
|
|
50
|
-
out
|
|
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
|
|
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():
|
|
@@ -9,11 +9,14 @@ from ciphergate.detectors.custom_regex import CustomRegexDetector
|
|
|
9
9
|
from ciphergate.detectors.dates import DateDetector
|
|
10
10
|
from ciphergate.detectors.email import EmailDetector, is_reserved_email_domain
|
|
11
11
|
from ciphergate.detectors.field_names import FieldNameDetector, PolicyClassificationDetector
|
|
12
|
+
from ciphergate.detectors.iban import IbanDetector
|
|
12
13
|
from ciphergate.detectors.ip_address import IPAddressDetector
|
|
14
|
+
from ciphergate.detectors.mac_address import MacAddressDetector
|
|
13
15
|
from ciphergate.detectors.network import InternalHostDetector
|
|
14
16
|
from ciphergate.detectors.payment_card import PaymentCardDetector
|
|
15
17
|
from ciphergate.detectors.phone import PhoneDetector
|
|
16
18
|
from ciphergate.detectors.secrets import PEM_BEGIN_RE, PEM_END_RE, SecretDetector
|
|
19
|
+
from ciphergate.detectors.ssn import SsnDetector
|
|
17
20
|
from ciphergate.models.finding import Finding, ScanItem
|
|
18
21
|
from ciphergate.models.policy import DetectorSettings, PolicyDocument
|
|
19
22
|
|
|
@@ -26,10 +29,23 @@ BUILTIN_DETECTOR_IDS: tuple[str, ...] = (
|
|
|
26
29
|
"email_regex",
|
|
27
30
|
"internal_host",
|
|
28
31
|
"phone_pattern",
|
|
32
|
+
"mac_address",
|
|
33
|
+
"iban",
|
|
34
|
+
"ssn_pattern",
|
|
29
35
|
)
|
|
30
36
|
|
|
31
37
|
# Phone matching is the least specific pattern; drop phone spans that overlap these detectors.
|
|
32
|
-
_PHONE_SUPPRESSORS = frozenset(
|
|
38
|
+
_PHONE_SUPPRESSORS = frozenset(
|
|
39
|
+
{
|
|
40
|
+
"luhn_card",
|
|
41
|
+
"ip_address",
|
|
42
|
+
"date_pattern",
|
|
43
|
+
"secret_pattern",
|
|
44
|
+
"mac_address",
|
|
45
|
+
"iban",
|
|
46
|
+
"ssn_pattern",
|
|
47
|
+
}
|
|
48
|
+
)
|
|
33
49
|
|
|
34
50
|
|
|
35
51
|
FIELD_CONTEXT_DETECTORS = frozenset({"field_name", "policy_classification"})
|
|
@@ -59,6 +75,9 @@ class DetectorSet:
|
|
|
59
75
|
"email_regex": EmailDetector(),
|
|
60
76
|
"internal_host": InternalHostDetector(settings.internal_host_suffixes),
|
|
61
77
|
"phone_pattern": PhoneDetector(),
|
|
78
|
+
"mac_address": MacAddressDetector(),
|
|
79
|
+
"iban": IbanDetector(),
|
|
80
|
+
"ssn_pattern": SsnDetector(),
|
|
62
81
|
}
|
|
63
82
|
for detector_id in BUILTIN_DETECTOR_IDS:
|
|
64
83
|
if detector_id not in disabled:
|
|
@@ -173,6 +192,12 @@ def unsafe_replacement_reason(value: str) -> str | None:
|
|
|
173
192
|
return f"looks like a credential ({secrets[0].evidence[-1]})"
|
|
174
193
|
if PaymentCardDetector().detect(probe):
|
|
175
194
|
return "looks like a payment card number"
|
|
195
|
+
if IbanDetector().detect(probe):
|
|
196
|
+
return "looks like a real IBAN"
|
|
197
|
+
if MacAddressDetector().detect(probe):
|
|
198
|
+
return "looks like a real MAC address"
|
|
199
|
+
if SsnDetector().detect(probe):
|
|
200
|
+
return "looks like a real government identifier"
|
|
176
201
|
for f in EmailDetector().detect(probe):
|
|
177
202
|
if "reserved_domain" not in f.evidence:
|
|
178
203
|
return "email replacements must use a reserved domain such as example.invalid"
|
|
@@ -65,20 +65,72 @@ _PATTERNS: tuple[_Pattern, ...] = (
|
|
|
65
65
|
"assignment",
|
|
66
66
|
re.compile(
|
|
67
67
|
r"(?i)(?<![a-z0-9])"
|
|
68
|
-
r"(?:password|passwd|passphrase|secret|
|
|
68
|
+
r"(?:(?:pg|db|mysql|admin|root|user)?password|mysql_pwd|passwd|passphrase|secret|"
|
|
69
|
+
r"api[_-]?key|apikey|access[_-]?token|"
|
|
69
70
|
r"refresh[_-]?token|auth[_-]?token|client[_-]?secret|secret[_-]?key|private[_-]?key|"
|
|
70
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"
|
|
71
84
|
r"(?:\\?[\"'])?\s{0,3}[:=]\s{0,3}(?:\\?[\"'])?"
|
|
72
85
|
r"([^\s\"'\\,;&}]{1,512})"
|
|
73
86
|
),
|
|
74
87
|
1,
|
|
75
88
|
0.8,
|
|
76
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
|
+
),
|
|
77
109
|
)
|
|
78
110
|
|
|
79
111
|
_PLACEHOLDER_VALUES = frozenset(
|
|
80
|
-
{
|
|
112
|
+
{
|
|
113
|
+
"",
|
|
114
|
+
"null",
|
|
115
|
+
"none",
|
|
116
|
+
"nil",
|
|
117
|
+
"undefined",
|
|
118
|
+
"true",
|
|
119
|
+
"false",
|
|
120
|
+
"redacted",
|
|
121
|
+
"***",
|
|
122
|
+
"******",
|
|
123
|
+
"xxx",
|
|
124
|
+
"{",
|
|
125
|
+
"[",
|
|
126
|
+
}
|
|
81
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]:\\)")
|
|
82
134
|
|
|
83
135
|
|
|
84
136
|
def _is_placeholder(value: str) -> bool:
|
|
@@ -109,7 +161,7 @@ class SecretDetector:
|
|
|
109
161
|
for m in pattern.regex.finditer(text):
|
|
110
162
|
start, end = m.start(pattern.group), m.end(pattern.group)
|
|
111
163
|
value = m.group(pattern.group)
|
|
112
|
-
if _is_placeholder(value):
|
|
164
|
+
if _is_placeholder(value) or self._is_reference(pattern.name, m, value):
|
|
113
165
|
continue
|
|
114
166
|
if any(s <= start and end <= e for s, e in covered):
|
|
115
167
|
continue
|
|
@@ -117,6 +169,15 @@ class SecretDetector:
|
|
|
117
169
|
out.append(self._finding(item, (start, end), pattern.name, pattern.confidence))
|
|
118
170
|
return out
|
|
119
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
|
+
|
|
120
181
|
def _finding(
|
|
121
182
|
self, item: ScanItem, span: tuple[int, int], family: str, confidence: float
|
|
122
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"))]
|