scrufflehog 0.1.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.
- scrufflehog-0.1.0/.github/workflows/release.yml +86 -0
- scrufflehog-0.1.0/.gitignore +7 -0
- scrufflehog-0.1.0/LICENSE +21 -0
- scrufflehog-0.1.0/PKG-INFO +116 -0
- scrufflehog-0.1.0/README.md +96 -0
- scrufflehog-0.1.0/docs/AGENTIC.md +63 -0
- scrufflehog-0.1.0/examples/scrufflehog.toml +53 -0
- scrufflehog-0.1.0/pyproject.toml +34 -0
- scrufflehog-0.1.0/src/scrufflehog/__init__.py +14 -0
- scrufflehog-0.1.0/src/scrufflehog/advisor.py +39 -0
- scrufflehog-0.1.0/src/scrufflehog/advisors/__init__.py +2 -0
- scrufflehog-0.1.0/src/scrufflehog/advisors/llm.py +167 -0
- scrufflehog-0.1.0/src/scrufflehog/cli.py +82 -0
- scrufflehog-0.1.0/src/scrufflehog/config.py +55 -0
- scrufflehog-0.1.0/src/scrufflehog/coverage/__init__.py +4 -0
- scrufflehog-0.1.0/src/scrufflehog/coverage/extract.py +104 -0
- scrufflehog-0.1.0/src/scrufflehog/coverage/semantics.py +19 -0
- scrufflehog-0.1.0/src/scrufflehog/engine.py +134 -0
- scrufflehog-0.1.0/src/scrufflehog/oracles.py +92 -0
- scrufflehog-0.1.0/src/scrufflehog/output/__init__.py +7 -0
- scrufflehog-0.1.0/src/scrufflehog/output/json_out.py +16 -0
- scrufflehog-0.1.0/src/scrufflehog/output/sarif.py +77 -0
- scrufflehog-0.1.0/src/scrufflehog/output/text.py +18 -0
- scrufflehog-0.1.0/src/scrufflehog/probes.py +79 -0
- scrufflehog-0.1.0/src/scrufflehog/runners/__init__.py +31 -0
- scrufflehog-0.1.0/src/scrufflehog/runners/go_runner.py +71 -0
- scrufflehog-0.1.0/src/scrufflehog/runners/node_runner.py +64 -0
- scrufflehog-0.1.0/src/scrufflehog/runners/python_runner.py +58 -0
- scrufflehog-0.1.0/src/scrufflehog/runners/rust_runner.py +67 -0
- scrufflehog-0.1.0/tests/test_advisor.py +99 -0
- scrufflehog-0.1.0/tests/test_live_runners.py +111 -0
- scrufflehog-0.1.0/tests/test_scrufflehog.py +190 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes scrufflehog to PyPI when a GitHub Release is published, using PyPI
|
|
4
|
+
# Trusted Publishing (OIDC): the job mints a short-lived OpenID Connect token
|
|
5
|
+
# that PyPI exchanges for upload rights. No API token is stored in the repo or
|
|
6
|
+
# in Actions secrets.
|
|
7
|
+
#
|
|
8
|
+
# One-time PyPI setup (pending publisher, since the project doesn't exist yet):
|
|
9
|
+
# PyPI -> Your projects -> Publishing -> Add a pending publisher
|
|
10
|
+
# PyPI project name: scrufflehog
|
|
11
|
+
# Owner: seanturner83
|
|
12
|
+
# Repository: scrufflehog
|
|
13
|
+
# Workflow filename: release.yml
|
|
14
|
+
# Environment: pypi
|
|
15
|
+
# The first successful run creates the project and claims the name.
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
release:
|
|
19
|
+
types: [published]
|
|
20
|
+
# Manual trigger for re-runs / dry checks (still gated on the pypi environment).
|
|
21
|
+
workflow_dispatch:
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
build:
|
|
28
|
+
name: Build sdist + wheel
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
|
|
33
|
+
- uses: actions/setup-python@v5
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.11"
|
|
36
|
+
|
|
37
|
+
- name: Build
|
|
38
|
+
run: |
|
|
39
|
+
python -m pip install --upgrade build
|
|
40
|
+
python -m build
|
|
41
|
+
|
|
42
|
+
- name: Check metadata
|
|
43
|
+
run: |
|
|
44
|
+
python -m pip install --upgrade twine
|
|
45
|
+
twine check dist/*
|
|
46
|
+
|
|
47
|
+
- name: Verify tag matches package version
|
|
48
|
+
if: github.event_name == 'release'
|
|
49
|
+
run: |
|
|
50
|
+
# Release tag is v<version>; the built wheel must match, or we'd
|
|
51
|
+
# publish a version that disagrees with the tag people pinned.
|
|
52
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
53
|
+
whl=$(ls dist/*.whl | head -1)
|
|
54
|
+
pkg_ver=$(basename "$whl" | sed -E 's/^scrufflehog-([^-]+)-.*/\1/')
|
|
55
|
+
echo "tag=$tag wheel=$pkg_ver"
|
|
56
|
+
if [ "$tag" != "$pkg_ver" ]; then
|
|
57
|
+
echo "::error::release tag ($tag) != package version ($pkg_ver) — bump pyproject.toml or fix the tag"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
- uses: actions/upload-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
name: dist
|
|
64
|
+
path: dist/
|
|
65
|
+
|
|
66
|
+
publish:
|
|
67
|
+
name: Publish to PyPI
|
|
68
|
+
needs: build
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
# The pypi environment is the second gate: Trusted Publishing on PyPI is
|
|
71
|
+
# scoped to this environment name, and it can carry a required-reviewer
|
|
72
|
+
# protection rule so a human approves each real publish.
|
|
73
|
+
environment:
|
|
74
|
+
name: pypi
|
|
75
|
+
url: https://pypi.org/p/scrufflehog
|
|
76
|
+
permissions:
|
|
77
|
+
id-token: write # OIDC token for Trusted Publishing — the only privilege needed
|
|
78
|
+
steps:
|
|
79
|
+
- uses: actions/download-artifact@v4
|
|
80
|
+
with:
|
|
81
|
+
name: dist
|
|
82
|
+
path: dist/
|
|
83
|
+
|
|
84
|
+
- name: Publish
|
|
85
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
86
|
+
# No `with: password:` — auth is the OIDC identity of this job.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sean Turner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scrufflehog
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministically verify that your redactors actually redact.
|
|
5
|
+
Project-URL: Homepage, https://github.com/seanturner83/scrufflehog
|
|
6
|
+
Author: Sean Turner
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: logging,pii,redaction,secrets,security,testing
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Provides-Extra: agentic
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# scrufflehog
|
|
22
|
+
|
|
23
|
+
**Unit-test your redaction.**
|
|
24
|
+
|
|
25
|
+
Everyone scans for secrets that already leaked (trufflehog, gitleaks). Almost
|
|
26
|
+
nobody tests whether the redaction they *rely on* actually works. scrufflehog is
|
|
27
|
+
the inverse tool: it runs adversarial probes through your own redaction code and
|
|
28
|
+
deterministically asserts the secret is gone — and checks that your field
|
|
29
|
+
denylist/allow-list covers the sensitive names you think it does.
|
|
30
|
+
|
|
31
|
+
No model, no guessing: every verdict is a hard assertion against a planted secret
|
|
32
|
+
you control. Zero false positives by construction.
|
|
33
|
+
|
|
34
|
+
## Two things it checks
|
|
35
|
+
|
|
36
|
+
**1. Transform-strength** — *does the redactor's output still contain, or
|
|
37
|
+
trivially reverse to, the secret?* It executes your redactor on planted probes
|
|
38
|
+
and applies three oracles:
|
|
39
|
+
|
|
40
|
+
- `literal_survival` — the secret appears verbatim in the output.
|
|
41
|
+
- `noop_passthrough` — the "redactor" returned its input unchanged.
|
|
42
|
+
- `reversible` — the output is a keyless, low-entropy transform (truncated or
|
|
43
|
+
unsalted hash, base64, static substitution) that a bounded candidate space
|
|
44
|
+
recovers. Catches "redaction" that only *looks* redacted.
|
|
45
|
+
|
|
46
|
+
**2. Coverage** — *is every sensitive field name actually on your list?* A field
|
|
47
|
+
denylist/allow-list is data, not behaviour, so scrufflehog extracts it straight
|
|
48
|
+
from source and checks a sensitive-field corpus against it — **without executing
|
|
49
|
+
your code**. This works across languages (Go maps, Python collections,
|
|
50
|
+
pino/`fast-redact` path lists, Rust sets).
|
|
51
|
+
|
|
52
|
+
## Languages
|
|
53
|
+
|
|
54
|
+
| | transform-strength | coverage |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| Python | in-process import | ✓ |
|
|
57
|
+
| Go | driver built in your module | ✓ (map literals) |
|
|
58
|
+
| Rust | `cargo --example` driver | ✓ (set literals) |
|
|
59
|
+
| Node/JS | node driver via stdin | ✓ (pino path lists) |
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
Latest from source (works today):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install git+https://github.com/seanturner83/scrufflehog
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Once the first release is published, from PyPI:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install scrufflehog
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Use
|
|
76
|
+
|
|
77
|
+
Write a `scrufflehog.toml` declaring your redactors (see `examples/`):
|
|
78
|
+
|
|
79
|
+
```toml
|
|
80
|
+
[[transform]]
|
|
81
|
+
lang = "python"
|
|
82
|
+
module = "app/redact.py"
|
|
83
|
+
fn = "redact_value"
|
|
84
|
+
kind = "value"
|
|
85
|
+
|
|
86
|
+
[[coverage]]
|
|
87
|
+
module = "app/redact.py"
|
|
88
|
+
symbol = "SECRET_FIELDS"
|
|
89
|
+
extract = "py_collection"
|
|
90
|
+
match = "exact_ci"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Then:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
scrufflehog verify --config scrufflehog.toml --target . --format text
|
|
97
|
+
scrufflehog verify --config scrufflehog.toml --target . --format sarif # for code-scanning
|
|
98
|
+
scrufflehog verify --config scrufflehog.toml --target . --fail-on-defect # CI gate
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Deterministic by default; optional agentic assist
|
|
102
|
+
|
|
103
|
+
The core is entirely deterministic and that's the point. An **optional** advisor
|
|
104
|
+
(`--advisor llm`) can propose domain-matched probes, discover redactors, and
|
|
105
|
+
confirm coverage hypotheses against real field usage — but it only ever supplies
|
|
106
|
+
*inputs and hypotheses*; the deterministic oracle still renders every verdict.
|
|
107
|
+
With no advisor, output is fully reproducible. See `docs/AGENTIC.md`.
|
|
108
|
+
|
|
109
|
+
## Why "scrufflehog"
|
|
110
|
+
|
|
111
|
+
trufflehog finds the secrets. scrufflehog scruffs through the code that's
|
|
112
|
+
*supposed to hide them* and checks it actually does.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# scrufflehog
|
|
2
|
+
|
|
3
|
+
**Unit-test your redaction.**
|
|
4
|
+
|
|
5
|
+
Everyone scans for secrets that already leaked (trufflehog, gitleaks). Almost
|
|
6
|
+
nobody tests whether the redaction they *rely on* actually works. scrufflehog is
|
|
7
|
+
the inverse tool: it runs adversarial probes through your own redaction code and
|
|
8
|
+
deterministically asserts the secret is gone — and checks that your field
|
|
9
|
+
denylist/allow-list covers the sensitive names you think it does.
|
|
10
|
+
|
|
11
|
+
No model, no guessing: every verdict is a hard assertion against a planted secret
|
|
12
|
+
you control. Zero false positives by construction.
|
|
13
|
+
|
|
14
|
+
## Two things it checks
|
|
15
|
+
|
|
16
|
+
**1. Transform-strength** — *does the redactor's output still contain, or
|
|
17
|
+
trivially reverse to, the secret?* It executes your redactor on planted probes
|
|
18
|
+
and applies three oracles:
|
|
19
|
+
|
|
20
|
+
- `literal_survival` — the secret appears verbatim in the output.
|
|
21
|
+
- `noop_passthrough` — the "redactor" returned its input unchanged.
|
|
22
|
+
- `reversible` — the output is a keyless, low-entropy transform (truncated or
|
|
23
|
+
unsalted hash, base64, static substitution) that a bounded candidate space
|
|
24
|
+
recovers. Catches "redaction" that only *looks* redacted.
|
|
25
|
+
|
|
26
|
+
**2. Coverage** — *is every sensitive field name actually on your list?* A field
|
|
27
|
+
denylist/allow-list is data, not behaviour, so scrufflehog extracts it straight
|
|
28
|
+
from source and checks a sensitive-field corpus against it — **without executing
|
|
29
|
+
your code**. This works across languages (Go maps, Python collections,
|
|
30
|
+
pino/`fast-redact` path lists, Rust sets).
|
|
31
|
+
|
|
32
|
+
## Languages
|
|
33
|
+
|
|
34
|
+
| | transform-strength | coverage |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Python | in-process import | ✓ |
|
|
37
|
+
| Go | driver built in your module | ✓ (map literals) |
|
|
38
|
+
| Rust | `cargo --example` driver | ✓ (set literals) |
|
|
39
|
+
| Node/JS | node driver via stdin | ✓ (pino path lists) |
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
Latest from source (works today):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install git+https://github.com/seanturner83/scrufflehog
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Once the first release is published, from PyPI:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install scrufflehog
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Use
|
|
56
|
+
|
|
57
|
+
Write a `scrufflehog.toml` declaring your redactors (see `examples/`):
|
|
58
|
+
|
|
59
|
+
```toml
|
|
60
|
+
[[transform]]
|
|
61
|
+
lang = "python"
|
|
62
|
+
module = "app/redact.py"
|
|
63
|
+
fn = "redact_value"
|
|
64
|
+
kind = "value"
|
|
65
|
+
|
|
66
|
+
[[coverage]]
|
|
67
|
+
module = "app/redact.py"
|
|
68
|
+
symbol = "SECRET_FIELDS"
|
|
69
|
+
extract = "py_collection"
|
|
70
|
+
match = "exact_ci"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
scrufflehog verify --config scrufflehog.toml --target . --format text
|
|
77
|
+
scrufflehog verify --config scrufflehog.toml --target . --format sarif # for code-scanning
|
|
78
|
+
scrufflehog verify --config scrufflehog.toml --target . --fail-on-defect # CI gate
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Deterministic by default; optional agentic assist
|
|
82
|
+
|
|
83
|
+
The core is entirely deterministic and that's the point. An **optional** advisor
|
|
84
|
+
(`--advisor llm`) can propose domain-matched probes, discover redactors, and
|
|
85
|
+
confirm coverage hypotheses against real field usage — but it only ever supplies
|
|
86
|
+
*inputs and hypotheses*; the deterministic oracle still renders every verdict.
|
|
87
|
+
With no advisor, output is fully reproducible. See `docs/AGENTIC.md`.
|
|
88
|
+
|
|
89
|
+
## Why "scrufflehog"
|
|
90
|
+
|
|
91
|
+
trufflehog finds the secrets. scrufflehog scruffs through the code that's
|
|
92
|
+
*supposed to hide them* and checks it actually does.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Optional agentic layer — design
|
|
2
|
+
|
|
3
|
+
## Principle (non-negotiable)
|
|
4
|
+
|
|
5
|
+
scrufflehog's core is **entirely deterministic** and stays that way. Every
|
|
6
|
+
verdict — defect or clean — comes from a hard assertion against known ground
|
|
7
|
+
truth (planted probe → run redactor → literal/hash-space/set-membership check).
|
|
8
|
+
Same input, same answer, zero false positives, no model. That determinism is the
|
|
9
|
+
product's whole thesis: the deterministic counterpart to probabilistic LLM
|
|
10
|
+
review.
|
|
11
|
+
|
|
12
|
+
The agentic layer is **optional, off by default, and never renders a verdict.**
|
|
13
|
+
Rule: *the agent proposes INPUTS and HYPOTHESES; the deterministic oracle still
|
|
14
|
+
decides.* This preserves the zero-FP guarantee while removing scrufflehog's two
|
|
15
|
+
real blind spots (hand-authored probes, unconfirmed coverage hypotheses).
|
|
16
|
+
|
|
17
|
+
## Where an advisor adds value (three seams)
|
|
18
|
+
|
|
19
|
+
1. **Probe generation** — read the redactor's signature/body and generate
|
|
20
|
+
probes that match its INPUT DOMAIN. Solves the real footgun (feeding bare
|
|
21
|
+
values to a URL-path redactor → false positives until domain-matched probes
|
|
22
|
+
were hand-written). Generated probes are still run through the deterministic
|
|
23
|
+
oracle; the agent supplies inputs, not verdicts.
|
|
24
|
+
|
|
25
|
+
2. **Redactor + field discovery** — scan a repo and PROPOSE the registry
|
|
26
|
+
(redaction fns, denylist symbols, sensitive field names). Human/config
|
|
27
|
+
confirms. Discovery, not judgement.
|
|
28
|
+
|
|
29
|
+
3. **Coverage-gap confirmation** — the honest-caveat killer. A coverage finding
|
|
30
|
+
is a hypothesis ("the list lacks `ssn`" — but does a field named `ssn` reach
|
|
31
|
+
this redactor?). An advisor greps the codebase for real field usage to
|
|
32
|
+
confirm or refute, turning a hypothesis into a confirmed finding or dropping
|
|
33
|
+
it.
|
|
34
|
+
|
|
35
|
+
## Interface
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
class Advisor(Protocol):
|
|
39
|
+
def propose_probes(self, redactor_src: str, entry: dict) -> list[Probe]: ...
|
|
40
|
+
def discover_redactors(self, target: Path) -> list[dict]: ...
|
|
41
|
+
def confirm_coverage_gap(self, target: Path, field: str, redactor: str) -> Verdict: ...
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `NoopAdvisor` (default): `propose_probes` → [], `discover` → [], `confirm` →
|
|
45
|
+
UNCONFIRMED (finding stands as a hypothesis, exactly today's behaviour).
|
|
46
|
+
- `LLMAdvisor` (optional module, extra dependency): implements the three via a
|
|
47
|
+
model. Lives behind `pip install scrufflehog[agentic]`.
|
|
48
|
+
|
|
49
|
+
CLI:
|
|
50
|
+
```
|
|
51
|
+
scrufflehog verify --config x.toml --target . # pure deterministic
|
|
52
|
+
scrufflehog verify --config x.toml --target . --advisor llm # + agentic assist
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Invariants
|
|
56
|
+
|
|
57
|
+
- A defect is ALWAYS confirmed by the deterministic oracle. The advisor can add
|
|
58
|
+
probes that trigger one, or downgrade a coverage hypothesis to
|
|
59
|
+
confirmed/refuted, but it cannot manufacture a defect the oracle didn't verify.
|
|
60
|
+
- With `--advisor` absent, output is byte-identical to today. Reproducibility of
|
|
61
|
+
the deterministic path is a test invariant.
|
|
62
|
+
- Advisor failures (timeout, API error) degrade to the deterministic result,
|
|
63
|
+
never crash the run.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Example scrufflehog config. Point module paths at your redactors (relative to
|
|
2
|
+
# the --target checkout). Delete the languages you don't use.
|
|
3
|
+
|
|
4
|
+
# --- transform-strength: execute the redactor on probes ---------------------
|
|
5
|
+
|
|
6
|
+
[[transform]]
|
|
7
|
+
lang = "python"
|
|
8
|
+
module = "app/redact.py"
|
|
9
|
+
fn = "redact_value" # def redact_value(s: str) -> str
|
|
10
|
+
kind = "value"
|
|
11
|
+
probe_set = "value" # "value" (any sensitive value) | "url_apikey" (URL-scoped)
|
|
12
|
+
|
|
13
|
+
[[transform]]
|
|
14
|
+
lang = "go"
|
|
15
|
+
module = "internal/logredact"
|
|
16
|
+
import = "github.com/example/app/internal/logredact"
|
|
17
|
+
fn = "Sanitize" # func Sanitize(s string) string
|
|
18
|
+
kind = "value"
|
|
19
|
+
# wrap = "error" # set if the fn takes an error, not a string
|
|
20
|
+
|
|
21
|
+
[[transform]]
|
|
22
|
+
lang = "node"
|
|
23
|
+
module = "dist/redact.js"
|
|
24
|
+
fn = "redactValue" # module.exports.redactValue = (s) => ...
|
|
25
|
+
kind = "value"
|
|
26
|
+
# export = "default" # set if module.exports IS the fn
|
|
27
|
+
|
|
28
|
+
[[transform]]
|
|
29
|
+
lang = "rust"
|
|
30
|
+
module = "." # crate root (Cargo.toml dir)
|
|
31
|
+
call = "app::redact::mask(&line)"
|
|
32
|
+
|
|
33
|
+
# --- coverage: static field-list check (no execution) -----------------------
|
|
34
|
+
|
|
35
|
+
[[coverage]]
|
|
36
|
+
module = "app/redact.py"
|
|
37
|
+
symbol = "SECRET_FIELDS" # a Python list/tuple/set of field names
|
|
38
|
+
extract = "py_collection"
|
|
39
|
+
match = "exact_ci"
|
|
40
|
+
# corpus = ["ssn", "cvv", "iban"] # optional; omit to use the built-in list
|
|
41
|
+
|
|
42
|
+
[[coverage]]
|
|
43
|
+
module = "internal/logredact/keys.go"
|
|
44
|
+
symbol = "SecretKeys" # var SecretKeys = map[string]struct{}{ "password": {}, ... }
|
|
45
|
+
extract = "go_map_keys"
|
|
46
|
+
match = "exact_ci"
|
|
47
|
+
doc_claims_substring = false
|
|
48
|
+
|
|
49
|
+
[[coverage]]
|
|
50
|
+
module = "src/logger.ts"
|
|
51
|
+
symbol = "redact" # pino redact: { paths: [ ... ] } or redact: [ ... ]
|
|
52
|
+
extract = "ts_redact_paths"
|
|
53
|
+
match = "exact_ci"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scrufflehog"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Deterministically verify that your redactors actually redact."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Sean Turner" }]
|
|
13
|
+
keywords = ["security", "redaction", "pii", "secrets", "logging", "testing"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [] # core is stdlib-only (tomllib on 3.11+)
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
agentic = [] # LLM advisor deps go here (opt-in)
|
|
25
|
+
dev = ["pytest>=7"]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
scrufflehog = "scrufflehog.cli:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/seanturner83/scrufflehog"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/scrufflehog"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""scrufflehog — deterministically verify that your redactors actually redact.
|
|
2
|
+
|
|
3
|
+
Everyone scans for secrets that leaked. scrufflehog tests whether the redaction
|
|
4
|
+
you rely on actually works: it runs adversarial probes through your own redactor
|
|
5
|
+
and asserts the secret is gone and not trivially reversible, and checks your
|
|
6
|
+
field denylist/allow-list covers the sensitive names you think it does.
|
|
7
|
+
"""
|
|
8
|
+
from .oracles import Defect, assert_output, reversible
|
|
9
|
+
from .probes import Probe, get_probe_set
|
|
10
|
+
from .engine import RunResult, run
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
__all__ = ["Defect", "assert_output", "reversible", "Probe", "get_probe_set",
|
|
14
|
+
"RunResult", "run"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Optional advisor interface — the ONLY place non-determinism may enter.
|
|
2
|
+
|
|
3
|
+
An advisor proposes INPUTS and HYPOTHESES; it never renders a verdict. The
|
|
4
|
+
deterministic oracle still decides every defect. The default NoopAdvisor makes
|
|
5
|
+
the engine byte-identical to a pure deterministic run — the agentic layer is
|
|
6
|
+
strictly additive and opt-in. See docs/AGENTIC.md.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
from .probes import Probe
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CoverageVerdict:
|
|
17
|
+
CONFIRMED = "confirmed" # a real field with this name reaches the redactor
|
|
18
|
+
REFUTED = "refuted" # no such field is logged here — drop the finding
|
|
19
|
+
UNCONFIRMED = "unconfirmed" # can't tell — finding stands as a hypothesis
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class Advisor(Protocol):
|
|
24
|
+
def propose_probes(self, redactor_src: str, entry: dict) -> list[Probe]: ...
|
|
25
|
+
def discover_redactors(self, target: Path) -> list[dict]: ...
|
|
26
|
+
def confirm_coverage_gap(self, target: Path, field: str, redactor: str) -> str: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NoopAdvisor:
|
|
30
|
+
"""Default. Adds nothing; the run stays purely deterministic."""
|
|
31
|
+
|
|
32
|
+
def propose_probes(self, redactor_src: str, entry: dict) -> list[Probe]:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
def discover_redactors(self, target: Path) -> list[dict]:
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
def confirm_coverage_gap(self, target: Path, field: str, redactor: str) -> str:
|
|
39
|
+
return CoverageVerdict.UNCONFIRMED
|