redac---n 0.0.0-stage → 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +323 -2
- package/THIRD_PARTY_NOTICES.md +59 -0
- package/dist/aho.d.ts +25 -0
- package/dist/aho.js +116 -0
- package/dist/base64.d.ts +10 -0
- package/dist/base64.js +29 -0
- package/dist/bip39-data.d.ts +2 -0
- package/dist/bip39-data.js +2 -0
- package/dist/cache.d.ts +14 -0
- package/dist/cache.js +40 -0
- package/dist/calls.d.ts +64 -0
- package/dist/calls.js +269 -0
- package/dist/charlm-data.d.ts +2 -0
- package/dist/charlm-data.js +3 -0
- package/dist/charlm.d.ts +7 -0
- package/dist/charlm.js +65 -0
- package/dist/checksums.d.ts +10 -0
- package/dist/checksums.js +98 -0
- package/dist/commands.d.ts +72 -0
- package/dist/commands.js +784 -0
- package/dist/context.d.ts +98 -0
- package/dist/context.js +469 -0
- package/dist/encoded.d.ts +11 -0
- package/dist/encoded.js +336 -0
- package/dist/environment.d.ts +10 -0
- package/dist/environment.js +62 -0
- package/dist/formats-data.d.ts +2 -0
- package/dist/formats-data.js +405 -0
- package/dist/formats.d.ts +5 -0
- package/dist/formats.js +470 -0
- package/dist/hmac.d.ts +3 -0
- package/dist/hmac.js +87 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +484 -0
- package/dist/json.d.ts +82 -0
- package/dist/json.js +392 -0
- package/dist/known.d.ts +34 -0
- package/dist/known.js +340 -0
- package/dist/lines.d.ts +28 -0
- package/dist/lines.js +69 -0
- package/dist/lookup.d.ts +5 -0
- package/dist/lookup.js +7 -0
- package/dist/model-data.d.ts +3 -0
- package/dist/model-data.js +4 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +19 -0
- package/dist/named.d.ts +20 -0
- package/dist/named.js +3257 -0
- package/dist/names.d.ts +94 -0
- package/dist/names.js +1449 -0
- package/dist/normalize.d.ts +25 -0
- package/dist/normalize.js +529 -0
- package/dist/pem.d.ts +32 -0
- package/dist/pem.js +269 -0
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +470 -0
- package/dist/public.d.ts +44 -0
- package/dist/public.js +362 -0
- package/dist/publickeys.d.ts +41 -0
- package/dist/publickeys.js +232 -0
- package/dist/random.d.ts +29 -0
- package/dist/random.js +165 -0
- package/dist/repeats.d.ts +21 -0
- package/dist/repeats.js +76 -0
- package/dist/rules.d.ts +42 -0
- package/dist/rules.js +58 -0
- package/dist/runs.d.ts +10 -0
- package/dist/runs.js +36 -0
- package/dist/secretfile.d.ts +52 -0
- package/dist/secretfile.js +676 -0
- package/dist/shell.d.ts +127 -0
- package/dist/shell.js +703 -0
- package/dist/spans.d.ts +53 -0
- package/dist/spans.js +272 -0
- package/dist/sql.d.ts +20 -0
- package/dist/sql.js +66 -0
- package/dist/stream.d.ts +46 -0
- package/dist/stream.js +150 -0
- package/dist/structure.d.ts +24 -0
- package/dist/structure.js +1614 -0
- package/dist/syntax.d.ts +156 -0
- package/dist/syntax.js +446 -0
- package/dist/transcript.d.ts +30 -0
- package/dist/transcript.js +709 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +1 -0
- package/dist/unnamed.d.ts +59 -0
- package/dist/unnamed.js +939 -0
- package/dist/urls.d.ts +70 -0
- package/dist/urls.js +303 -0
- package/dist/values.d.ts +178 -0
- package/dist/values.js +1621 -0
- package/dist/yaml.d.ts +40 -0
- package/dist/yaml.js +174 -0
- package/docs/creddata.svg +318 -0
- package/docs/guessing.md +139 -0
- package/docs/holdout.svg +85 -0
- package/docs/layers.md +80 -0
- package/docs/models.svg +81 -0
- package/docs/real-text.svg +112 -0
- package/package.json +57 -5
package/docs/guessing.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# How redac---n guesses
|
|
2
|
+
|
|
3
|
+
Most secrets come with evidence: a credential's name (`DB_PASSWORD=`), a vendor's format (`ghp_…`), a structure
|
|
4
|
+
(a PEM block, a URL's password), a value you gave it, or the command that printed it (`cat .env`). What's left are
|
|
5
|
+
random-looking strings with none of that next to them: the key passed to `Client("…")`, the token in a neutral
|
|
6
|
+
`X-Lumen:` header, a value pasted into chat. Deciding those is guessing, and the `guessing` option sets how much of it
|
|
7
|
+
the library does. This page explains how the guessing layer works, how well it does, and why it's built this way.
|
|
8
|
+
[Why each layer is there](layers.md) compares it with the other detectors.
|
|
9
|
+
|
|
10
|
+
## The problem
|
|
11
|
+
|
|
12
|
+
A candidate is any run of 16 or more token characters (letters, digits, `+/=_-`). Deciding whether it's a secret
|
|
13
|
+
takes two questions, and each needs different evidence:
|
|
14
|
+
|
|
15
|
+
1. **Did a machine generate it?** Keys, hashes, UUIDs, nonces and certificate data are generated; identifiers
|
|
16
|
+
(`get_all_subclasses`), words, paths and versions are written by people. The token itself answers this: how much
|
|
17
|
+
it reads like an identifier to a character model trained on real code (`charlm.ts`), how its entropy compares
|
|
18
|
+
with a random string of the same length and alphabet (`random.ts`), how often it switches between cases and
|
|
19
|
+
digits, and how long it is.
|
|
20
|
+
2. **If so, is it a secret here?** Commit hashes, digests, object IDs, publishable keys and API keys are all random.
|
|
21
|
+
Only the context tells them apart: the name it's under (`api_key=` versus `sha256:`), words nearby ("publishable",
|
|
22
|
+
"anon", "trace"), where it sits (a column of IDs, a file listing, a SQL row, a URL path), or a function it's
|
|
23
|
+
passed to.
|
|
24
|
+
|
|
25
|
+
Some facts settle a candidate whatever the answers: an encryption wrapper (`ENC[…]`, `{cipher}`), a public key's
|
|
26
|
+
structure, a file's magic number in base64, a checksum that verifies, an API object ID's format (`toolu_…`,
|
|
27
|
+
`vpc-0bddc3783c4aae5fc`).
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
`src/unnamed.ts` first reads each candidate's context (`src/context.ts`): the name it's under, the syntax around it
|
|
32
|
+
(an assignment, a flag, a header, a call, an item in a named list), and the words on its line. A token whose run
|
|
33
|
+
starts with `_`, `-` or `+` is read from where the run starts, so `Client("_abc…")` is still a call. Then three
|
|
34
|
+
things decide it:
|
|
35
|
+
|
|
36
|
+
- **Vetoes**: about 60 hand-written rules (72 in all) that set a candidate aside. Seven of them are facts
|
|
37
|
+
(encryption, public keys, AWS unique IDs, hash prefixes, key IDs after the option that takes them), shared with the
|
|
38
|
+
decision stage that reviews every detector's findings. The rest judge how a token looks (hex, a UUID, an
|
|
39
|
+
identifier, a slug, words) or its context (a hash's or an ID's name, a file listing, a SQL cell, a path).
|
|
40
|
+
- **A LightGBM forest** (300 trees, 43 features) scores whatever the vetoes let through. Most features describe the
|
|
41
|
+
token; a few describe its context coarsely (a credential word on the line, the kind of name, the syntax).
|
|
42
|
+
- **A threshold**, set by `guessing`: 0.8 for `"confident"` (the default), 0.5 for `"eager"`, and none at all for
|
|
43
|
+
`"off"`, which turns the layer off.
|
|
44
|
+
|
|
45
|
+
Inside a secret file (a hint, or a secret file written inside the text) the layer always runs at 0.5, whatever
|
|
46
|
+
`guessing` says. There the hint has already said the text holds secrets, so the model's job is different: it sorts
|
|
47
|
+
the file's values under neutral names into generated ones, which are hidden (`SALT=k2Lm9Qx`), and settings, which
|
|
48
|
+
stay (`PORT=8080`, a hostname, a log level).
|
|
49
|
+
|
|
50
|
+
The forest is good at the first question and crude at the second; the vetoes carry most of the context, as
|
|
51
|
+
exceptions.
|
|
52
|
+
|
|
53
|
+
The forest is trained by `scripts/unnamed-data.ts` and `scripts/train-unnamed.py`. Its negatives are real candidates
|
|
54
|
+
from public repositories and agent trajectories. Its positives are random keys placed where keys appear (in many
|
|
55
|
+
alphabets and lengths, including standard base64 up to the 172 characters `openssl rand -base64 128` prints). Both
|
|
56
|
+
pass through the same vetoes the forest sits behind, so it only learns what the vetoes leave to it. That matters for
|
|
57
|
+
hex: the hex veto lets hex through only where a key goes (a credential's name, a handover, a vendor client's call), so
|
|
58
|
+
the training data places hex keys in vendor calls too, or the forest would never see one there. Each tree is fitted on
|
|
59
|
+
a random 80% of the rows and of the features.
|
|
60
|
+
|
|
61
|
+
## Measurements
|
|
62
|
+
|
|
63
|
+
On the development sets (4,250 cases) and 58 M characters of ordinary text (agent trajectories and repositories; the
|
|
64
|
+
same corpus the negatives come from):
|
|
65
|
+
|
|
66
|
+
- **The vetoes do most of the work.** Without them, the pipeline makes 74 redactions per million characters of
|
|
67
|
+
ordinary text instead of 5.1, and destroys 7.8% of the look-alikes in the development sets instead of 1.4%, for
|
|
68
|
+
1.1 points more recall. A few vetoes (hex, identifier-shaped, object IDs, public IDs, hash words, hex groups,
|
|
69
|
+
placeholders, UUIDs) do most of that; the rest matter for the cases they name. They also throw away some secrets,
|
|
70
|
+
mostly hex keys the hex veto mistakes for hashes.
|
|
71
|
+
- **Context matters.** Scoring each token without the text around it costs 2.1 points of recall and destroys 2.4% of
|
|
72
|
+
the look-alikes instead of 1.4%.
|
|
73
|
+
- **The forest mostly measures randomness.** Five features (two forms of word-likeness, length, entropy, and whether
|
|
74
|
+
the token has a name) account for nearly 80% of its gain. On the hardest candidates, the ones only the forest
|
|
75
|
+
decides, it ranks weakly (AUC about 0.7): a little over half of what it scores above 0.8 is secret, and about the
|
|
76
|
+
same share above 0.995.
|
|
77
|
+
- **Its strongest context signal comes from how it's trained.** Real candidates next to a credential word are left
|
|
78
|
+
out of the negatives, because in public text they're often examples or leaked keys. So the forest treats "a
|
|
79
|
+
credential word on the line" as near proof. That's wrong for a publishable key next to "key", but in agent output
|
|
80
|
+
a random string next to `api_key=` is usually a secret: training without that exclusion costs about 2 points of
|
|
81
|
+
recall.
|
|
82
|
+
- **The score tracks randomness, not certainty.** The share of secrets rises with the score, from about 5% below 0.05
|
|
83
|
+
to over half above 0.995 on the hardest development cases. In ordinary text, everything above 0.8 adds up to a
|
|
84
|
+
fraction of a redaction per million characters. Scores between 0.5 and 0.8, which `"eager"` adds, are mostly
|
|
85
|
+
look-alikes no veto covers: ETags, ULIDs, `go.sum` hashes.
|
|
86
|
+
|
|
87
|
+
## Why a forest
|
|
88
|
+
|
|
89
|
+
Several other designs were built and scored the same way. The cleanest of them factors the two questions: one model
|
|
90
|
+
scores how generated the token looks, another how secret its context makes it, and the vetoes about appearance and
|
|
91
|
+
context become things the model learns instead of rules. It was built as one logistic sum over about 2,000 named
|
|
92
|
+
pieces of evidence, and as a product, P(generated | token) × P(secret | generated, context). For training labels, a
|
|
93
|
+
large language model was asked what about 17,000 hard candidates were; it separated them far better than any feature
|
|
94
|
+
model (AUC 0.90 against the forest's 0.7).
|
|
95
|
+
|
|
96
|
+

|
|
97
|
+
|
|
98
|
+
None of them catches as many secrets as the forest at the same rate of destroyed look-alikes. Moving a threshold
|
|
99
|
+
moves a model along its own line, and the forest's line sits above the rest along its whole length. On the held-out
|
|
100
|
+
sets, scored once, the best alternative recalled about 0.4 points less than the forest at the same loss.
|
|
101
|
+
|
|
102
|
+
Why the forest wins, and what would have to change for that to change:
|
|
103
|
+
|
|
104
|
+
| reason | what it means |
|
|
105
|
+
|---|---|
|
|
106
|
+
| The training text is public text, not agent output | In public text, 64-character base64 is almost always a line of a PEM body or a blob, so a model that reads context learns that length as evidence against a secret. The forest's simpler "random enough is a secret" is right for agent output. |
|
|
107
|
+
| The positives are synthetic | Every key in the training data was placed by a generator, and the readable models learn its habits: an ID's place, or a public word in the name, counts toward a secret because the generator puts keys there more often than real text does. |
|
|
108
|
+
| The hardest cases need knowledge | That `serverKey` in a SCRAM block is a derived hash, or that a Segment write key in `api_tokens` is a credential, isn't in the 40 characters around a token. The language model knows it; a small model trained on its labels learns only part of it. |
|
|
109
|
+
| What could change it | Real labelled secrets instead of placed ones (the 2,600 the language model labelled are the only real positives any of these models has seen), sign constraints on the context flags the generator distorts, and key lengths in whole bytes in neutral places. |
|
|
110
|
+
|
|
111
|
+
Other designs that were tried:
|
|
112
|
+
|
|
113
|
+
| design | result |
|
|
114
|
+
|---|---|
|
|
115
|
+
| the forest trained on cleaner data (hard negatives kept, key lengths spread evenly) | about 0.4 points less recall; it reads length smoothly and the credential-word shortcut is gone |
|
|
116
|
+
| simpler models (a monotone forest of depth 2; an additive model of 6 KB) | 1 to 2 points less recall; the additive model is 20 times smaller and faster |
|
|
117
|
+
| predicting the kind of token (secret, hash, ID, public key, encoded, code) | no better separation; calibration on the training data doesn't carry over to agent output |
|
|
118
|
+
| giving the forest the vetoes' context as features | it can replace the vetoes about appearance and recovers hex keys where keys go, but recalls 2 points less than the forest with its vetoes |
|
|
119
|
+
| a linear model of the 40 characters before the token | ranks better (AUC 0.76) and is calibrated, and learns "public", "id", "state" and "nonce" as evidence against; on its own, about 0.6 points less recall |
|
|
120
|
+
|
|
121
|
+
On the hardest candidates, random tokens next to credential words, every model sits near 50% precision: telling a
|
|
122
|
+
publishable key from a secret one there needs knowledge the text doesn't hold.
|
|
123
|
+
|
|
124
|
+
## Rebuilding the model
|
|
125
|
+
|
|
126
|
+
The forest is trained from a corpus of ordinary text (one JSON object per line, `{"src": …, "text": …}`, at
|
|
127
|
+
`$REDACT_CORPORA/built/dev.jsonl`):
|
|
128
|
+
|
|
129
|
+
node scripts/unnamed-data.ts > train.jsonl
|
|
130
|
+
python3 -I scripts/train-unnamed.py train.jsonl --write src/model-data.ts
|
|
131
|
+
|
|
132
|
+
The corpus behind the shipped model (634 MB of public repositories and agent trajectories) isn't published: its
|
|
133
|
+
sources' licences differ from this one's. With your own corpus the same two commands train a model of the same
|
|
134
|
+
design, not the same model. Both steps are deterministic (the random keys are seeded, and so is LightGBM's training,
|
|
135
|
+
including its sampling of rows and features), so a given corpus always gives the same model.
|
|
136
|
+
|
|
137
|
+
The training data passes through the vetoes and the same context reading, so any change to a veto or to how context
|
|
138
|
+
is read changes the data, and the model has to be retrained with it. `node scripts/model-chart.ts` redraws the chart
|
|
139
|
+
above from the measurements written in it.
|
package/docs/holdout.svg
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" width="760" height="460" role="img" aria-labelledby="t d">
|
|
2
|
+
<title id="t">Held-out test set: secrets caught vs. look-alikes destroyed</title><desc id="d">redac---n guessing off: 90.9% of secrets caught, 1.1% of keep values lost; redac---n guessing confident: 93.0% of secrets caught, 3.0% of keep values lost; redac---n guessing eager: 93.2% of secrets caught, 3.2% of keep values lost; gitleaks: 54.6% of secrets caught, 1.5% of keep values lost; betterleaks: 67.0% of secrets caught, 1.7% of keep values lost; kingfisher: 43.1% of secrets caught, 0.5% of keep values lost; trufflehog: 38.5% of secrets caught, 0.4% of keep values lost; noseyparker: 42.1% of secrets caught, 1.5% of keep values lost; detect-secrets: 47.2% of secrets caught, 15.2% of keep values lost; credsweeper: 70.1% of secrets caught, 15.2% of keep values lost; union(all 7): 84.2% of secrets caught, 28.6% of keep values lost; union(all but detect-secrets): 83.1% of secrets caught, 16.9% of keep values lost; gitleaks+betterleaks+kingfisher: 70.6% of secrets caught, 2.2% of keep values lost</desc>
|
|
3
|
+
<style>
|
|
4
|
+
.bg { fill: #fcfcfb; } .accent { fill: #2a78d6; } .accent-line { stroke: #2a78d6; } .other { fill: #898781; }
|
|
5
|
+
.hollow { fill: none; stroke: #898781; } .chain { fill: none; stroke: #c3c2b7; stroke-width: 1.5; }
|
|
6
|
+
.k0 { fill: #2a78d6; } .k1 { fill: #eb6834; } .k2 { fill: #1baf7a; } .k3 { fill: #b9b8b2; } .gap { stroke: #fcfcfb; stroke-width: 2; }
|
|
7
|
+
.ring { stroke: #fcfcfb; } .grid { stroke: #e1e0d9; stroke-width: 1; } .axis, .lead { stroke: #c3c2b7; stroke-width: 1; }
|
|
8
|
+
text { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; fill: #52514e; font-size: 12px; }
|
|
9
|
+
.title { fill: #0b0b0b; font-size: 15px; font-weight: 600; } .panel { fill: #0b0b0b; font-size: 13px; font-weight: 600; }
|
|
10
|
+
.tick { fill: #898781; font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
11
|
+
.ours { fill: #0b0b0b; font-weight: 600; }
|
|
12
|
+
@media (prefers-color-scheme: dark) {
|
|
13
|
+
.bg { fill: #1a1a19; } .accent { fill: #3987e5; } .accent-line { stroke: #3987e5; } .ring { stroke: #1a1a19; }
|
|
14
|
+
.other { fill: #a3a29c; } .hollow { stroke: #a3a29c; } .chain { stroke: #4a4a47; }
|
|
15
|
+
.k0 { fill: #3987e5; } .k1 { fill: #d95926; } .k2 { fill: #199e70; } .k3 { fill: #5e5d58; } .gap { stroke: #1a1a19; }
|
|
16
|
+
.grid { stroke: #2c2c2a; } .axis, .lead { stroke: #383835; }
|
|
17
|
+
text { fill: #c3c2b7; } .title, .panel, .ours { fill: #ffffff; }
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
20
|
+
<rect width="760" height="460" rx="8" class="bg"/>
|
|
21
|
+
<text x="16" y="26" class="title">Held-out test set: secrets caught vs. look-alikes destroyed</text>
|
|
22
|
+
<text x="16" y="44" class="sub">1112 cases, 1554 secrets, 3115 look-alike values that must survive; never used for tuning</text>
|
|
23
|
+
<line class="grid" x1="56" x2="736" y1="408" y2="408"/>
|
|
24
|
+
<text class="tick" x="48" y="412" text-anchor="end">30%</text>
|
|
25
|
+
<line class="grid" x1="56" x2="736" y1="358.85714285714283" y2="358.85714285714283"/>
|
|
26
|
+
<text class="tick" x="48" y="362.85714285714283" text-anchor="end">40%</text>
|
|
27
|
+
<line class="grid" x1="56" x2="736" y1="309.71428571428567" y2="309.71428571428567"/>
|
|
28
|
+
<text class="tick" x="48" y="313.71428571428567" text-anchor="end">50%</text>
|
|
29
|
+
<line class="grid" x1="56" x2="736" y1="260.57142857142856" y2="260.57142857142856"/>
|
|
30
|
+
<text class="tick" x="48" y="264.57142857142856" text-anchor="end">60%</text>
|
|
31
|
+
<line class="grid" x1="56" x2="736" y1="211.42857142857144" y2="211.42857142857144"/>
|
|
32
|
+
<text class="tick" x="48" y="215.42857142857144" text-anchor="end">70%</text>
|
|
33
|
+
<line class="grid" x1="56" x2="736" y1="162.28571428571428" y2="162.28571428571428"/>
|
|
34
|
+
<text class="tick" x="48" y="166.28571428571428" text-anchor="end">80%</text>
|
|
35
|
+
<line class="grid" x1="56" x2="736" y1="113.14285714285722" y2="113.14285714285722"/>
|
|
36
|
+
<text class="tick" x="48" y="117.14285714285722" text-anchor="end">90%</text>
|
|
37
|
+
<line class="grid" x1="56" x2="736" y1="64" y2="64"/>
|
|
38
|
+
<text class="tick" x="48" y="68" text-anchor="end">100%</text>
|
|
39
|
+
<line class="grid" x1="56" x2="56" y1="64" y2="408"/>
|
|
40
|
+
<text class="tick" x="56" y="426" text-anchor="middle">0%</text>
|
|
41
|
+
<line class="grid" x1="180.0111861486094" x2="180.0111861486094" y1="64" y2="408"/>
|
|
42
|
+
<text class="tick" x="180.0111861486094" y="426" text-anchor="middle">1%</text>
|
|
43
|
+
<line class="grid" x1="231.37830133733794" x2="231.37830133733794" y1="64" y2="408"/>
|
|
44
|
+
<text class="tick" x="231.37830133733794" y="426" text-anchor="middle">2%</text>
|
|
45
|
+
<line class="grid" x1="333.297442198671" x2="333.297442198671" y1="64" y2="408"/>
|
|
46
|
+
<text class="tick" x="333.297442198671" y="426" text-anchor="middle">5%</text>
|
|
47
|
+
<line class="grid" x1="448.1578035687299" x2="448.1578035687299" y1="64" y2="408"/>
|
|
48
|
+
<text class="tick" x="448.1578035687299" y="426" text-anchor="middle">10%</text>
|
|
49
|
+
<line class="grid" x1="610.594884397342" x2="610.594884397342" y1="64" y2="408"/>
|
|
50
|
+
<text class="tick" x="610.594884397342" y="426" text-anchor="middle">20%</text>
|
|
51
|
+
<line class="grid" x1="735.2372403656558" x2="735.2372403656558" y1="64" y2="408"/>
|
|
52
|
+
<text class="tick" x="735.2372403656558" y="426" text-anchor="middle">30%</text>
|
|
53
|
+
<line class="axis" x1="56" x2="736" y1="408" y2="408"/>
|
|
54
|
+
<text x="736" y="450" text-anchor="end">text that must survive but was redacted (lower is better, √ scale) →</text>
|
|
55
|
+
<text transform="translate(14 408) rotate(-90)">secrets caught (higher is better) →</text>
|
|
56
|
+
<polyline fill="none" class="accent-line" stroke-width="2" points="183.64059086206942,108.90531347674198 272.5676772508076,98.46957161242875 278.1937486206931,97.52086780658203"/>
|
|
57
|
+
<circle cx="624.92" cy="74" r="6" class="accent"/><text x="635.92" y="78" class="ours">redac---n</text>
|
|
58
|
+
<circle cx="624.92" cy="94" r="5" class="other"/><text x="635.92" y="98">other scanners</text>
|
|
59
|
+
<circle cx="209.94034469409104" cy="287.26162897591473" r="5" class="other ring" stroke-width="2"><title>gitleaks: 54.6% caught, 1.5% lost</title></circle>
|
|
60
|
+
<circle cx="219.27839244707948" cy="226.22835079977938" r="5" class="other ring" stroke-width="2"><title>betterleaks: 67.0% caught, 1.7% lost</title></circle>
|
|
61
|
+
<circle cx="144.8774994482772" cy="343.86762272476557" r="5" class="other ring" stroke-width="2"><title>kingfisher: 43.1% caught, 0.5% lost</title></circle>
|
|
62
|
+
<circle cx="136.1130953739465" cy="366.0040448611877" r="5" class="other ring" stroke-width="2"><title>trufflehog: 38.5% caught, 0.4% lost</title></circle>
|
|
63
|
+
<circle cx="208.32835949118075" cy="348.29490715205" r="5" class="other ring" stroke-width="2"><title>noseyparker: 42.1% caught, 1.5% lost</title></circle>
|
|
64
|
+
<circle cx="540.2600480620406" cy="323.62860820003675" r="5" class="other ring" stroke-width="2"><title>detect-secrets: 47.2% caught, 15.2% lost</title></circle>
|
|
65
|
+
<circle cx="539.750032072132" cy="211.04908990623272" r="5" class="other ring" stroke-width="2"><title>credsweeper: 70.1% caught, 15.2% lost</title></circle>
|
|
66
|
+
<circle cx="719.6120496050027" cy="141.47747747747746" r="5" class="other ring" stroke-width="2"><title>union(all 7): 84.2% caught, 28.6% lost</title></circle>
|
|
67
|
+
<circle cx="565.5944718414523" cy="147.16970031255744" r="5" class="other ring" stroke-width="2"><title>union(all but detect-secrets): 83.1% caught, 16.9% lost</title></circle>
|
|
68
|
+
<circle cx="239.2256589830112" cy="208.51921309064167" r="5" class="other ring" stroke-width="2"><title>gitleaks+betterleaks+kingfisher: 70.6% caught, 2.2% lost</title></circle>
|
|
69
|
+
<circle cx="183.64059086206942" cy="108.90531347674198" r="6" class="accent ring" stroke-width="2"><title>redac---n guessing off: 90.9% caught, 1.1% lost</title></circle>
|
|
70
|
+
<circle cx="272.5676772508076" cy="98.46957161242875" r="6" class="accent ring" stroke-width="2"><title>redac---n guessing confident: 93.0% caught, 3.0% lost</title></circle>
|
|
71
|
+
<circle cx="278.1937486206931" cy="97.52086780658203" r="6" class="accent ring" stroke-width="2"><title>redac---n guessing eager: 93.2% caught, 3.2% lost</title></circle>
|
|
72
|
+
<text x="288.1937486206931" y="101.52086780658203" text-anchor="start" class="ours">guessing eager</text>
|
|
73
|
+
<text x="264.5676772508076" y="88.46957161242875" text-anchor="end" class="ours">guessing confident</text>
|
|
74
|
+
<text x="173.64059086206942" y="112.90531347674198" text-anchor="end" class="ours">guessing off</text>
|
|
75
|
+
<text x="709.6120496050027" y="145.47747747747746" text-anchor="end">all seven combined</text>
|
|
76
|
+
<text x="555.5944718414523" y="151.16970031255744" text-anchor="end">all but detect-secrets</text>
|
|
77
|
+
<text x="249.2256589830112" y="212.51921309064167" text-anchor="start">gitleaks + Betterleaks + Kingfisher</text>
|
|
78
|
+
<text x="549.750032072132" y="215.04908990623272" text-anchor="start">CredSweeper</text>
|
|
79
|
+
<text x="229.27839244707948" y="230.22835079977938" text-anchor="start">Betterleaks</text>
|
|
80
|
+
<text x="219.94034469409104" y="291.26162897591473" text-anchor="start">gitleaks</text>
|
|
81
|
+
<text x="550.2600480620406" y="327.62860820003675" text-anchor="start">detect-secrets</text>
|
|
82
|
+
<text x="134.8774994482772" y="347.86762272476557" text-anchor="end">Kingfisher</text>
|
|
83
|
+
<text x="218.32835949118075" y="352.29490715205" text-anchor="start">Nosey Parker</text>
|
|
84
|
+
<text x="146.1130953739465" y="370.0040448611877" text-anchor="start">TruffleHog</text>
|
|
85
|
+
</svg>
|
package/docs/layers.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Why each layer is there
|
|
2
|
+
|
|
3
|
+
Besides the values you give it, which are hidden wherever they appear, redac---n has six detectors and a decision
|
|
4
|
+
stage that reviews what they find. Each detector looks for a different kind of evidence, and each catches secrets
|
|
5
|
+
the others miss. This page shows what each one does, what only it catches, and what it costs in false redactions,
|
|
6
|
+
as measured by `node eval/layers.ts`.
|
|
7
|
+
|
|
8
|
+
The numbers come from the development sets (4,250 cases holding 6,424 secrets and 18,402 values that look like
|
|
9
|
+
secrets but aren't) and 58 M characters of ordinary text: agent trajectories and repositories with almost no
|
|
10
|
+
secrets, so nearly every redaction there is a mistake. The guessing model's training data comes from that same text,
|
|
11
|
+
so its numbers there are a little flattering. With everything on, redac---n catches 95.4% of the secrets, destroys
|
|
12
|
+
1.4% of the look-alikes and makes 5.1 redactions per million characters of ordinary text.
|
|
13
|
+
|
|
14
|
+
## What each layer does
|
|
15
|
+
|
|
16
|
+
In the table, "recall without it" is the full pipeline with only that layer turned off. The next two columns count
|
|
17
|
+
the secrets and look-alikes no other layer hides, and the last the redactions in ordinary text no other layer makes.
|
|
18
|
+
|
|
19
|
+
| layer | recall without it | secrets only it catches | look-alikes only it destroys | ordinary-text redactions only it makes |
|
|
20
|
+
|---|---:|---:|---:|---:|
|
|
21
|
+
| names | 84.1% (−11.3 points) | 788 | 20 | 1.64 per M characters |
|
|
22
|
+
| structure | 89.5% (−5.9) | 372 | 7 | 1.32 per M |
|
|
23
|
+
| random tokens | 90.0% (−5.4) | 362 | 140 | 0.62 per M |
|
|
24
|
+
| formats | 91.2% (−4.2) | 233 | 17 | 0.09 per M |
|
|
25
|
+
| where the text came from | 94.6% (−0.8) | 50 | 3 | 0.02 per M |
|
|
26
|
+
| encoded | 95.3% (−0.1) | 6 | 0 | 0 |
|
|
27
|
+
|
|
28
|
+
**Names** (`src/named.ts`) finds values under a credential's name, in every syntax: env files, YAML, JSON, INI,
|
|
29
|
+
code, flags, XML, tables, SQL rows, sentences. It's the biggest layer because most secrets in agent output have a
|
|
30
|
+
name next to them.
|
|
31
|
+
|
|
32
|
+
**Structure** (`src/structure.ts`) knows where secrets go in formats that have a place for them: a private key's
|
|
33
|
+
PEM block (judged by its contents, not its label), a URL's password, an `Authorization` header, a cookie, a
|
|
34
|
+
connection string, a command-line tool's password flag. These often have no name to read, or a name that public
|
|
35
|
+
values share: a `CERTIFICATE` block can hold a private key.
|
|
36
|
+
|
|
37
|
+
**Random tokens** (`src/unnamed.ts`; see [How it guesses](guessing.md)) is the only layer that finds a secret with
|
|
38
|
+
no name, format or structure around it: the key passed to `Client("…")`, a token in a neutral header, a value pasted
|
|
39
|
+
into chat. It's also wrong most often: 140 look-alikes, mostly random IDs and hashes, are destroyed by it alone,
|
|
40
|
+
which is why `guessing` is a setting.
|
|
41
|
+
|
|
42
|
+
**Formats** (`src/formats.ts`) recognizes 536 vendor token formats by their prefixes and shapes (`ghp_`, `sk-ant-`,
|
|
43
|
+
`xoxb-`, `AKIA…`). On its own it catches a third of the secrets, but most of those have a name next to them too, so
|
|
44
|
+
what only it catches is smaller. It almost never fires on ordinary text: 0.09 false redactions per million
|
|
45
|
+
characters.
|
|
46
|
+
|
|
47
|
+
**Where the text came from** (`src/commands.ts`, `src/secretfile.ts`) uses the hint. What `cat .env` printed is a
|
|
48
|
+
secret file, so every value in it is hidden; what `gh auth token` printed is a token. Few development cases come with
|
|
49
|
+
a hint, so it measures small here. It's the layer that hides a 7-character `SALT` nothing else would, and the one
|
|
50
|
+
that keeps a transcript safe when a tool prints a whole secrets file.
|
|
51
|
+
|
|
52
|
+
**Encoded** (`src/encoded.ts`) decodes base64 and hex and checks what they hold with the rest of the pipeline. The
|
|
53
|
+
development sets have few encoded secrets, so it measures little here. It's there because encoding hides the
|
|
54
|
+
evidence every other layer looks for: a base64'd `.env`, a Kubernetes Secret's data, an `xxd` dump of a key file.
|
|
55
|
+
|
|
56
|
+
## The steps around them
|
|
57
|
+
|
|
58
|
+
| step turned off | recall | look-alikes destroyed | ordinary-text redactions per M characters |
|
|
59
|
+
|---|---:|---:|---:|
|
|
60
|
+
| none (the full pipeline) | 95.4% | 1.4% | 5.1 |
|
|
61
|
+
| the random layer's vetoes | 96.5% | 7.8% | 73.7 |
|
|
62
|
+
| only its statistical vetoes | 96.4% | 3.8% | 31.7 |
|
|
63
|
+
| its reading of a token's context | 93.3% | 2.4% | 7.7 |
|
|
64
|
+
| the decision stage (`src/policy.ts`) | 95.6% | 2.9% | 5.8 |
|
|
65
|
+
| learning a value and hiding its repeats | 94.6% | 1.4% | 5.0 |
|
|
66
|
+
| hints | 94.7% | 1.3% | 5.1 |
|
|
67
|
+
| normalizing text first (colours, entities, line numbers, split literals) | 95.0% | 1.4% | 5.2 |
|
|
68
|
+
|
|
69
|
+
The vetoes keep the random layer usable: hashes, UUIDs, IDs, public keys and encrypted values are random too, and
|
|
70
|
+
without the rules that set them aside the layer would redact 14 times as much ordinary text. The decision stage
|
|
71
|
+
halves the look-alikes destroyed, whichever detector found them: placeholders, references, types, keys that are
|
|
72
|
+
public by design, version strings.
|
|
73
|
+
|
|
74
|
+
## Running it
|
|
75
|
+
|
|
76
|
+
node eval/layers.ts [--guessing confident] [--every 10] [--no-real]
|
|
77
|
+
|
|
78
|
+
It scores every development set with each layer on its own, each layer left out and each step left out, and counts
|
|
79
|
+
the findings no other layer makes. The ordinary-text sample needs the external corpora (`$REDACT_CORPORA`); pass
|
|
80
|
+
`--no-real` without them.
|
package/docs/models.svg
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 480" width="760" height="480" role="img" aria-labelledby="t d">
|
|
2
|
+
<title id="t">Why a forest: secrets caught against look-alikes destroyed</title><desc id="d">the forest with its vetoes: 95.41% caught at 1.73% destroyed, 95.31% caught at 1.62% destroyed, 95.28% caught at 1.53% destroyed, 95.00% caught at 1.46% destroyed, 94.24% caught at 1.31% destroyed; one logistic sum, the statistical vetoes on: 95.30% caught at 1.89% destroyed, 95.17% caught at 1.84% destroyed, 95.00% caught at 1.76% destroyed, 94.79% caught at 1.62% destroyed; one logistic sum, with a language model's labels: 95.22% caught at 2.06% destroyed, 94.94% caught at 1.77% destroyed, 94.57% caught at 1.59% destroyed, 94.08% caught at 1.41% destroyed, 93.82% caught at 1.31% destroyed; one logistic sum, without them: 94.88% caught at 1.98% destroyed, 94.60% caught at 1.69% destroyed, 94.30% caught at 1.52% destroyed, 93.90% caught at 1.40% destroyed, 93.40% caught at 1.29% destroyed, 93.00% caught at 1.19% destroyed, 92.09% caught at 1.09% destroyed; a linear model of the 40 characters before the token: 94.70% caught at 1.54% destroyed; the product P(generated) × P(secret | generated): 93.26% caught at 1.27% destroyed; the forest given the vetoes' context as features: 93.20% caught at 1.26% destroyed</desc>
|
|
3
|
+
<style>
|
|
4
|
+
.bg { fill: #fcfcfb; } .s0 { fill: #2a78d6; stroke: #2a78d6; } .s1 { fill: #c98a00; stroke: #c98a00; } .s2 { fill: #c84f00; stroke: #c84f00; } .s3 { fill: #b4518d; stroke: #b4518d; } .s4 { fill: #00876a; stroke: #00876a; } .s5 { fill: #7a7974; stroke: #7a7974; } .s6 { fill: #8c6d1f; stroke: #8c6d1f; }
|
|
5
|
+
.ring { stroke: #fcfcfb; } .grid { stroke: #e1e0d9; stroke-width: 1; } .axis { stroke: #c3c2b7; stroke-width: 1; }
|
|
6
|
+
text { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; fill: #52514e; font-size: 12px; }
|
|
7
|
+
.title { fill: #0b0b0b; font-size: 15px; font-weight: 600; }
|
|
8
|
+
.tick { fill: #898781; font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
9
|
+
.ours { fill: #0b0b0b; font-weight: 600; }
|
|
10
|
+
@media (prefers-color-scheme: dark) {
|
|
11
|
+
.bg { fill: #1a1a19; } .s0 { fill: #3987e5; stroke: #3987e5; } .s1 { fill: #e69f00; stroke: #e69f00; } .s2 { fill: #e3712c; stroke: #e3712c; } .s3 { fill: #d38ab8; stroke: #d38ab8; } .s4 { fill: #2fb38f; stroke: #2fb38f; } .s5 { fill: #a3a29c; stroke: #a3a29c; } .s6 { fill: #b8964a; stroke: #b8964a; } .ring { stroke: #1a1a19; }
|
|
12
|
+
.grid { stroke: #2c2c2a; } .axis { stroke: #383835; }
|
|
13
|
+
text { fill: #c3c2b7; } .title, .ours { fill: #ffffff; }
|
|
14
|
+
}
|
|
15
|
+
</style>
|
|
16
|
+
<rect width="760" height="480" rx="8" class="bg"/>
|
|
17
|
+
<text x="16" y="26" class="title">Why a forest: secrets caught against look-alikes destroyed</text>
|
|
18
|
+
<text x="16" y="44">Development sets; each line is one model with its threshold swept. Up and to the left is better.</text>
|
|
19
|
+
<line class="grid" x1="64" x2="736" y1="428" y2="428"/>
|
|
20
|
+
<text class="tick" x="56" y="432" text-anchor="end">92%</text>
|
|
21
|
+
<line class="grid" x1="64" x2="736" y1="336.9999999999998" y2="336.9999999999998"/>
|
|
22
|
+
<text class="tick" x="56" y="340.9999999999998" text-anchor="end">93%</text>
|
|
23
|
+
<line class="grid" x1="64" x2="736" y1="245.9999999999995" y2="245.9999999999995"/>
|
|
24
|
+
<text class="tick" x="56" y="249.9999999999995" text-anchor="end">94%</text>
|
|
25
|
+
<line class="grid" x1="64" x2="736" y1="154.9999999999992" y2="154.9999999999992"/>
|
|
26
|
+
<text class="tick" x="56" y="158.9999999999992" text-anchor="end">95%</text>
|
|
27
|
+
<line class="grid" x1="64" x2="736" y1="63.99999999999898" y2="63.99999999999898"/>
|
|
28
|
+
<text class="tick" x="56" y="67.99999999999898" text-anchor="end">96%</text>
|
|
29
|
+
<line class="grid" x1="64" x2="64" y1="64" y2="428"/>
|
|
30
|
+
<text class="tick" x="64" y="446" text-anchor="middle">1.0%</text>
|
|
31
|
+
<line class="grid" x1="176" x2="176" y1="64" y2="428"/>
|
|
32
|
+
<text class="tick" x="176" y="446" text-anchor="middle">1.2%</text>
|
|
33
|
+
<line class="grid" x1="288" x2="288" y1="64" y2="428"/>
|
|
34
|
+
<text class="tick" x="288" y="446" text-anchor="middle">1.4%</text>
|
|
35
|
+
<line class="grid" x1="400.00000000000006" x2="400.00000000000006" y1="64" y2="428"/>
|
|
36
|
+
<text class="tick" x="400.00000000000006" y="446" text-anchor="middle">1.6%</text>
|
|
37
|
+
<line class="grid" x1="512.0000000000002" x2="512.0000000000002" y1="64" y2="428"/>
|
|
38
|
+
<text class="tick" x="512.0000000000002" y="446" text-anchor="middle">1.8%</text>
|
|
39
|
+
<line class="grid" x1="624.0000000000002" x2="624.0000000000002" y1="64" y2="428"/>
|
|
40
|
+
<text class="tick" x="624.0000000000002" y="446" text-anchor="middle">2.0%</text>
|
|
41
|
+
<line class="grid" x1="736.0000000000005" x2="736.0000000000005" y1="64" y2="428"/>
|
|
42
|
+
<text class="tick" x="736.0000000000005" y="446" text-anchor="middle">2.2%</text>
|
|
43
|
+
<line class="axis" x1="64" x2="736" y1="428" y2="428"/>
|
|
44
|
+
<text x="736" y="470" text-anchor="end">look-alike values destroyed (lower is better) →</text>
|
|
45
|
+
<text transform="translate(12 428) rotate(-90)">secrets caught (higher is better) →</text>
|
|
46
|
+
<polyline fill="none" class="s3" style="fill:none" stroke-width="2" points="612.8000000000002,165.92000000000007 450.4,191.40000000000035 355.2,218.70000000000041 288,255.10000000000053 226.4,300.5999999999996 170.40000000000003,336.9999999999998 114.39999999999999,419.8099999999999"/>
|
|
47
|
+
<polyline fill="none" class="s2" style="fill:none" stroke-width="2" points="657.6,134.9799999999994 495.20000000000005,160.4599999999997 394.4000000000001,194.13000000000005 293.6,238.72000000000028 237.60000000000005,262.37999999999977"/>
|
|
48
|
+
<polyline fill="none" class="s1" style="fill:none" stroke-width="2" points="562.4000000000001,127.70000000000016 534.4000000000001,139.52999999999992 489.60000000000014,155.00000000000023 411.19999999999993,174.1100000000002"/>
|
|
49
|
+
<polyline fill="none" class="s0" style="fill:none" stroke-width="2.5" points="472.80000000000007,117.69000000000028 411.19999999999993,126.7900000000003 360.8,129.51999999999998 321.6,155.00000000000023 237.60000000000005,224.15999999999983"/>
|
|
50
|
+
<circle cx="209.6" cy="318.7999999999997" r="4" class="s6 ring" stroke-width="2"><title>the forest given the vetoes' context as features: 93.20% caught, 1.26% destroyed</title></circle>
|
|
51
|
+
<circle cx="215.2" cy="313.34000000000026" r="4" class="s5 ring" stroke-width="2"><title>the product P(generated) × P(secret | generated): 93.26% caught, 1.27% destroyed</title></circle>
|
|
52
|
+
<circle cx="366.40000000000003" cy="182.30000000000035" r="4" class="s4 ring" stroke-width="2"><title>a linear model of the 40 characters before the token: 94.70% caught, 1.54% destroyed</title></circle>
|
|
53
|
+
<circle cx="612.8000000000002" cy="165.92000000000007" r="4" class="s3 ring" stroke-width="2"><title>one logistic sum, without them: 94.88% caught, 1.98% destroyed</title></circle>
|
|
54
|
+
<circle cx="450.4" cy="191.40000000000035" r="4" class="s3 ring" stroke-width="2"><title>one logistic sum, without them: 94.60% caught, 1.69% destroyed</title></circle>
|
|
55
|
+
<circle cx="355.2" cy="218.70000000000041" r="4" class="s3 ring" stroke-width="2"><title>one logistic sum, without them: 94.30% caught, 1.52% destroyed</title></circle>
|
|
56
|
+
<circle cx="288" cy="255.10000000000053" r="4" class="s3 ring" stroke-width="2"><title>one logistic sum, without them: 93.90% caught, 1.40% destroyed</title></circle>
|
|
57
|
+
<circle cx="226.4" cy="300.5999999999996" r="4" class="s3 ring" stroke-width="2"><title>one logistic sum, without them: 93.40% caught, 1.29% destroyed</title></circle>
|
|
58
|
+
<circle cx="170.40000000000003" cy="336.9999999999998" r="4" class="s3 ring" stroke-width="2"><title>one logistic sum, without them: 93.00% caught, 1.19% destroyed</title></circle>
|
|
59
|
+
<circle cx="114.39999999999999" cy="419.8099999999999" r="4" class="s3 ring" stroke-width="2"><title>one logistic sum, without them: 92.09% caught, 1.09% destroyed</title></circle>
|
|
60
|
+
<circle cx="657.6" cy="134.9799999999994" r="4" class="s2 ring" stroke-width="2"><title>one logistic sum, with a language model's labels: 95.22% caught, 2.06% destroyed</title></circle>
|
|
61
|
+
<circle cx="495.20000000000005" cy="160.4599999999997" r="4" class="s2 ring" stroke-width="2"><title>one logistic sum, with a language model's labels: 94.94% caught, 1.77% destroyed</title></circle>
|
|
62
|
+
<circle cx="394.4000000000001" cy="194.13000000000005" r="4" class="s2 ring" stroke-width="2"><title>one logistic sum, with a language model's labels: 94.57% caught, 1.59% destroyed</title></circle>
|
|
63
|
+
<circle cx="293.6" cy="238.72000000000028" r="4" class="s2 ring" stroke-width="2"><title>one logistic sum, with a language model's labels: 94.08% caught, 1.41% destroyed</title></circle>
|
|
64
|
+
<circle cx="237.60000000000005" cy="262.37999999999977" r="4" class="s2 ring" stroke-width="2"><title>one logistic sum, with a language model's labels: 93.82% caught, 1.31% destroyed</title></circle>
|
|
65
|
+
<circle cx="562.4000000000001" cy="127.70000000000016" r="4" class="s1 ring" stroke-width="2"><title>one logistic sum, the statistical vetoes on: 95.30% caught, 1.89% destroyed</title></circle>
|
|
66
|
+
<circle cx="534.4000000000001" cy="139.52999999999992" r="4" class="s1 ring" stroke-width="2"><title>one logistic sum, the statistical vetoes on: 95.17% caught, 1.84% destroyed</title></circle>
|
|
67
|
+
<circle cx="489.60000000000014" cy="155.00000000000023" r="4" class="s1 ring" stroke-width="2"><title>one logistic sum, the statistical vetoes on: 95.00% caught, 1.76% destroyed</title></circle>
|
|
68
|
+
<circle cx="411.19999999999993" cy="174.1100000000002" r="4" class="s1 ring" stroke-width="2"><title>one logistic sum, the statistical vetoes on: 94.79% caught, 1.62% destroyed</title></circle>
|
|
69
|
+
<circle cx="472.80000000000007" cy="117.69000000000028" r="5" class="s0 ring" stroke-width="2"><title>the forest with its vetoes: 95.41% caught, 1.73% destroyed</title></circle>
|
|
70
|
+
<circle cx="411.19999999999993" cy="126.7900000000003" r="5" class="s0 ring" stroke-width="2"><title>the forest with its vetoes: 95.31% caught, 1.62% destroyed</title></circle>
|
|
71
|
+
<circle cx="360.8" cy="129.51999999999998" r="5" class="s0 ring" stroke-width="2"><title>the forest with its vetoes: 95.28% caught, 1.53% destroyed</title></circle>
|
|
72
|
+
<circle cx="321.6" cy="155.00000000000023" r="5" class="s0 ring" stroke-width="2"><title>the forest with its vetoes: 95.00% caught, 1.46% destroyed</title></circle>
|
|
73
|
+
<circle cx="237.60000000000005" cy="224.15999999999983" r="5" class="s0 ring" stroke-width="2"><title>the forest with its vetoes: 94.24% caught, 1.31% destroyed</title></circle>
|
|
74
|
+
<line x1="406" x2="424" y1="298.5" y2="298.5" class="s0" stroke-width="2"/><circle cx="415" cy="298.5" r="4" class="s0"/><text x="432" y="302.5" class="ours">the forest with its vetoes</text>
|
|
75
|
+
<line x1="406" x2="424" y1="317.5" y2="317.5" class="s1" stroke-width="2"/><circle cx="415" cy="317.5" r="4" class="s1"/><text x="432" y="321.5">one logistic sum, the statistical vetoes on</text>
|
|
76
|
+
<line x1="406" x2="424" y1="336.5" y2="336.5" class="s2" stroke-width="2"/><circle cx="415" cy="336.5" r="4" class="s2"/><text x="432" y="340.5">one logistic sum, with a language model's labels</text>
|
|
77
|
+
<line x1="406" x2="424" y1="355.5" y2="355.5" class="s3" stroke-width="2"/><circle cx="415" cy="355.5" r="4" class="s3"/><text x="432" y="359.5">one logistic sum, without them</text>
|
|
78
|
+
<line x1="406" x2="424" y1="374.5" y2="374.5" class="s4" stroke-width="2"/><circle cx="415" cy="374.5" r="4" class="s4"/><text x="432" y="378.5">a linear model of the 40 characters before the token</text>
|
|
79
|
+
<line x1="406" x2="424" y1="393.5" y2="393.5" class="s5" stroke-width="2"/><circle cx="415" cy="393.5" r="4" class="s5"/><text x="432" y="397.5">the product P(generated) × P(secret | generated)</text>
|
|
80
|
+
<line x1="406" x2="424" y1="412.5" y2="412.5" class="s6" stroke-width="2"/><circle cx="415" cy="412.5" r="4" class="s6"/><text x="432" y="416.5">the forest given the vetoes' context as features</text>
|
|
81
|
+
</svg>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 438" width="760" height="438" role="img" aria-labelledby="t d">
|
|
2
|
+
<title id="t">Redactions per million characters of ordinary text</title><desc id="d">kingfisher: 0.8 per MB; trufflehog: 2.0 per MB; gitleaks: 2.3 per MB; redac---n guessing off: 3.3 per MB; redac---n guessing confident: 4.0 per MB; redac---n guessing eager: 4.3 per MB; noseyparker: 8.5 per MB; credsweeper: 11.6 per MB; betterleaks: 16.7 per MB; gitleaks+betterleaks+kingfisher: 17.3 per MB; detect-secrets: 26.0 per MB; union(all but detect-secrets): 28.9 per MB; union(all 7): 51.4 per MB</desc>
|
|
3
|
+
<style>
|
|
4
|
+
.bg { fill: #fcfcfb; } .accent { fill: #2a78d6; } .accent-line { stroke: #2a78d6; } .other { fill: #898781; }
|
|
5
|
+
.hollow { fill: none; stroke: #898781; } .chain { fill: none; stroke: #c3c2b7; stroke-width: 1.5; }
|
|
6
|
+
.k0 { fill: #2a78d6; } .k1 { fill: #eb6834; } .k2 { fill: #1baf7a; } .k3 { fill: #b9b8b2; } .gap { stroke: #fcfcfb; stroke-width: 2; }
|
|
7
|
+
.ring { stroke: #fcfcfb; } .grid { stroke: #e1e0d9; stroke-width: 1; } .axis, .lead { stroke: #c3c2b7; stroke-width: 1; }
|
|
8
|
+
text { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; fill: #52514e; font-size: 12px; }
|
|
9
|
+
.title { fill: #0b0b0b; font-size: 15px; font-weight: 600; } .panel { fill: #0b0b0b; font-size: 13px; font-weight: 600; }
|
|
10
|
+
.tick { fill: #898781; font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
11
|
+
.ours { fill: #0b0b0b; font-weight: 600; }
|
|
12
|
+
@media (prefers-color-scheme: dark) {
|
|
13
|
+
.bg { fill: #1a1a19; } .accent { fill: #3987e5; } .accent-line { stroke: #3987e5; } .ring { stroke: #1a1a19; }
|
|
14
|
+
.other { fill: #a3a29c; } .hollow { stroke: #a3a29c; } .chain { stroke: #4a4a47; }
|
|
15
|
+
.k0 { fill: #3987e5; } .k1 { fill: #d95926; } .k2 { fill: #199e70; } .k3 { fill: #5e5d58; } .gap { stroke: #1a1a19; }
|
|
16
|
+
.grid { stroke: #2c2c2a; } .axis, .lead { stroke: #383835; }
|
|
17
|
+
text { fill: #c3c2b7; } .title, .panel, .ours { fill: #ffffff; }
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
20
|
+
<rect width="760" height="438" rx="8" class="bg"/>
|
|
21
|
+
<text x="16" y="26" class="title">Redactions per million characters of ordinary text</text>
|
|
22
|
+
<text x="16" y="44" class="sub">60 M characters of agent trajectories and source repos never used in development, with few real secrets (lower is better)</text>
|
|
23
|
+
<line class="grid" x1="259.20000000000005" x2="259.20000000000005" y1="60" y2="402"/>
|
|
24
|
+
<text class="tick" x="259.20000000000005" y="418" text-anchor="middle">0</text>
|
|
25
|
+
<line class="grid" x1="402.1333333333333" x2="402.1333333333333" y1="60" y2="402"/>
|
|
26
|
+
<text class="tick" x="402.1333333333333" y="418" text-anchor="middle">20</text>
|
|
27
|
+
<line class="grid" x1="545.0666666666666" x2="545.0666666666666" y1="60" y2="402"/>
|
|
28
|
+
<text class="tick" x="545.0666666666666" y="418" text-anchor="middle">40</text>
|
|
29
|
+
<line class="grid" x1="688" x2="688" y1="60" y2="402"/>
|
|
30
|
+
<text class="tick" x="688" y="418" text-anchor="middle">60</text>
|
|
31
|
+
<text x="249.20000000000005" y="81" text-anchor="end">Kingfisher</text>
|
|
32
|
+
<rect x="259.20000000000005" y="69" width="4.395672180135534" height="16" class="k0 gap"><title>kingfisher: credential-shaped, 0.62 per MB</title></rect>
|
|
33
|
+
<rect x="263.5956721801356" y="69" width="1" height="16" class="k2 gap"><title>kingfisher: word or placeholder, 0.12 per MB</title></rect>
|
|
34
|
+
<rect x="264.4272858358369" y="69" width="1" height="16" class="k3 gap"><title>kingfisher: other, 0.05 per MB</title></rect>
|
|
35
|
+
<text class="tick" x="270.7836916882803" y="81">0.8</text>
|
|
36
|
+
<text x="249.20000000000005" y="107" text-anchor="end">TruffleHog</text>
|
|
37
|
+
<rect x="259.20000000000005" y="95" width="8.672542409456582" height="16" class="k0 gap"><title>trufflehog: credential-shaped, 1.21 per MB</title></rect>
|
|
38
|
+
<rect x="267.8725424094566" y="95" width="4.395672180135591" height="16" class="k2 gap"><title>trufflehog: word or placeholder, 0.62 per MB</title></rect>
|
|
39
|
+
<rect x="272.2682145895922" y="95" width="1.1880195081446914" height="16" class="k3 gap"><title>trufflehog: other, 0.17 per MB</title></rect>
|
|
40
|
+
<text class="tick" x="279.4562340977369" y="107">2.0</text>
|
|
41
|
+
<text x="249.20000000000005" y="133" text-anchor="end">gitleaks</text>
|
|
42
|
+
<rect x="259.20000000000005" y="121" width="9.622958015972415" height="16" class="k0 gap"><title>gitleaks: credential-shaped, 1.35 per MB</title></rect>
|
|
43
|
+
<rect x="268.82295801597246" y="121" width="4.395672180135534" height="16" class="k1 gap"><title>gitleaks: hex, 0.62 per MB</title></rect>
|
|
44
|
+
<rect x="273.218630196108" y="121" width="2.2572370654750102" height="16" class="k3 gap"><title>gitleaks: other, 0.32 per MB</title></rect>
|
|
45
|
+
<text class="tick" x="281.475867261583" y="133">2.3</text>
|
|
46
|
+
<text x="249.20000000000005" y="159" text-anchor="end" class="ours">guessing off</text>
|
|
47
|
+
<rect x="259.20000000000005" y="147" width="11.285493821388854" height="16" class="k0 gap"><title>redac---n guessing off: credential-shaped, 1.58 per MB</title></rect>
|
|
48
|
+
<rect x="270.4854938213889" y="147" width="4.276608184947349" height="16" class="k1 gap"><title>redac---n guessing off: hex, 0.60 per MB</title></rect>
|
|
49
|
+
<rect x="274.76210200633625" y="147" width="4.157813513143253" height="16" class="k2 gap"><title>redac---n guessing off: word or placeholder, 0.58 per MB</title></rect>
|
|
50
|
+
<rect x="278.9199155194795" y="147" width="3.8014294977309646" height="16" class="k3 gap"><title>redac---n guessing off: other, 0.53 per MB</title></rect>
|
|
51
|
+
<text class="tick" x="288.72134501721047" y="159">3.3</text>
|
|
52
|
+
<text x="249.20000000000005" y="185" text-anchor="end" class="ours">guessing confident</text>
|
|
53
|
+
<rect x="259.20000000000005" y="173" width="13.780181929274761" height="16" class="k0 gap"><title>redac---n guessing confident: credential-shaped, 1.93 per MB</title></rect>
|
|
54
|
+
<rect x="272.9801819292748" y="173" width="6.296117605616928" height="16" class="k1 gap"><title>redac---n guessing confident: hex, 0.88 per MB</title></rect>
|
|
55
|
+
<rect x="279.27629953489173" y="173" width="4.157813513143253" height="16" class="k2 gap"><title>redac---n guessing confident: word or placeholder, 0.58 per MB</title></rect>
|
|
56
|
+
<path d="M283.434113048035,173 h0.5141975285555418 a4,4 0 0 1 4,4 v8 a4,4 0 0 1 -4,4 h-0.5141975285555418 z" class="k3 gap"><title>redac---n guessing confident: other, 0.63 per MB</title></path>
|
|
57
|
+
<text class="tick" x="293.94831057659053" y="185">4.0</text>
|
|
58
|
+
<text x="249.20000000000005" y="211" text-anchor="end" class="ours">guessing eager</text>
|
|
59
|
+
<rect x="259.20000000000005" y="199" width="14.849333975511627" height="16" class="k0 gap"><title>redac---n guessing eager: credential-shaped, 2.08 per MB</title></rect>
|
|
60
|
+
<rect x="274.0493339755117" y="199" width="6.414912277421024" height="16" class="k1 gap"><title>redac---n guessing eager: hex, 0.90 per MB</title></rect>
|
|
61
|
+
<rect x="280.4642462529327" y="199" width="4.157813513143253" height="16" class="k2 gap"><title>redac---n guessing eager: word or placeholder, 0.58 per MB</title></rect>
|
|
62
|
+
<path d="M284.62205976607595,199 h1.5833495747923507 a4,4 0 0 1 4,4 v8 a4,4 0 0 1 -4,4 h-1.5833495747923507 z" class="k3 gap"><title>redac---n guessing eager: other, 0.78 per MB</title></path>
|
|
63
|
+
<text class="tick" x="296.2054093408683" y="211">4.3</text>
|
|
64
|
+
<text x="249.20000000000005" y="237" text-anchor="end">Nosey Parker</text>
|
|
65
|
+
<rect x="259.20000000000005" y="225" width="2.8512468195473843" height="16" class="k0 gap"><title>noseyparker: credential-shaped, 0.40 per MB</title></rect>
|
|
66
|
+
<rect x="262.05124681954743" y="225" width="1" height="16" class="k1 gap"><title>noseyparker: hex, 0.13 per MB</title></rect>
|
|
67
|
+
<rect x="263.0016624260632" y="225" width="52.27285835836858" height="16" class="k2 gap"><title>noseyparker: word or placeholder, 7.31 per MB</title></rect>
|
|
68
|
+
<path d="M315.2745207844318,225 h0.5144741309500205 a4,4 0 0 1 4,4 v8 a4,4 0 0 1 -4,4 h-0.5144741309500205 z" class="k3 gap"><title>noseyparker: other, 0.63 per MB</title></path>
|
|
69
|
+
<text class="tick" x="325.7889949153818" y="237">8.5</text>
|
|
70
|
+
<text x="249.20000000000005" y="263" text-anchor="end">CredSweeper</text>
|
|
71
|
+
<rect x="259.20000000000005" y="251" width="21.503153097419784" height="16" class="k0 gap"><title>credsweeper: credential-shaped, 3.01 per MB</title></rect>
|
|
72
|
+
<rect x="280.70315309741983" y="251" width="49.065205686377794" height="16" class="k1 gap"><title>credsweeper: hex, 6.87 per MB</title></rect>
|
|
73
|
+
<rect x="329.7683587837976" y="251" width="2.0196331638460947" height="16" class="k2 gap"><title>credsweeper: word or placeholder, 0.28 per MB</title></rect>
|
|
74
|
+
<path d="M331.7879919476437,251 h6.573373622488191 a4,4 0 0 1 4,4 v8 a4,4 0 0 1 -4,4 h-6.573373622488191 z" class="k3 gap"><title>credsweeper: other, 1.48 per MB</title></path>
|
|
75
|
+
<text class="tick" x="348.3613655701319" y="263">12</text>
|
|
76
|
+
<text x="249.20000000000005" y="289" text-anchor="end">Betterleaks</text>
|
|
77
|
+
<rect x="259.20000000000005" y="277" width="16.27586726158296" height="16" class="k0 gap"><title>betterleaks: credential-shaped, 2.28 per MB</title></rect>
|
|
78
|
+
<rect x="275.475867261583" y="277" width="3.9204643768776464" height="16" class="k1 gap"><title>betterleaks: hex, 0.55 per MB</title></rect>
|
|
79
|
+
<rect x="279.39633163846065" y="277" width="87.79464165189631" height="16" class="k2 gap"><title>betterleaks: word or placeholder, 12.28 per MB</title></rect>
|
|
80
|
+
<path d="M367.19097329035696,277 h7.167383376560565 a4,4 0 0 1 4,4 v8 a4,4 0 0 1 -4,4 h-7.167383376560565 z" class="k3 gap"><title>betterleaks: other, 1.56 per MB</title></path>
|
|
81
|
+
<text class="tick" x="384.35835666691753" y="289">17</text>
|
|
82
|
+
<text x="249.20000000000005" y="315" text-anchor="end">gitleaks + Betterleaks + Kingfisher</text>
|
|
83
|
+
<rect x="259.20000000000005" y="303" width="19.602321884388232" height="16" class="k0 gap"><title>gitleaks+betterleaks+kingfisher: credential-shaped, 2.74 per MB</title></rect>
|
|
84
|
+
<rect x="278.8023218843883" y="303" width="4.395672180135534" height="16" class="k1 gap"><title>gitleaks+betterleaks+kingfisher: hex, 0.62 per MB</title></rect>
|
|
85
|
+
<rect x="283.1979940645238" y="303" width="87.79464165189631" height="16" class="k2 gap"><title>gitleaks+betterleaks+kingfisher: word or placeholder, 12.28 per MB</title></rect>
|
|
86
|
+
<path d="M370.9926357164201,303 h7.523789229004024 a4,4 0 0 1 4,4 v8 a4,4 0 0 1 -4,4 h-7.523789229004024 z" class="k3 gap"><title>gitleaks+betterleaks+kingfisher: other, 1.61 per MB</title></path>
|
|
87
|
+
<text class="tick" x="388.51642494542415" y="315">17</text>
|
|
88
|
+
<text x="249.20000000000005" y="341" text-anchor="end">detect-secrets</text>
|
|
89
|
+
<rect x="259.20000000000005" y="329" width="19.245916031944773" height="16" class="k0 gap"><title>detect-secrets: credential-shaped, 2.69 per MB</title></rect>
|
|
90
|
+
<rect x="278.4459160319448" y="329" width="41.34307888343699" height="16" class="k1 gap"><title>detect-secrets: hex, 5.78 per MB</title></rect>
|
|
91
|
+
<rect x="319.7889949153818" y="329" width="12.593006786334229" height="16" class="k2 gap"><title>detect-secrets: word or placeholder, 1.76 per MB</title></rect>
|
|
92
|
+
<path d="M332.38200170171604,329 h108.38664547049245 a4,4 0 0 1 4,4 v8 a4,4 0 0 1 -4,4 h-108.38664547049245 z" class="k3 gap"><title>detect-secrets: other, 15.73 per MB</title></path>
|
|
93
|
+
<text class="tick" x="450.7686471722086" y="341">26</text>
|
|
94
|
+
<text x="249.20000000000005" y="367" text-anchor="end">all but detect-secrets</text>
|
|
95
|
+
<rect x="259.20000000000005" y="355" width="28.0372603922159" height="16" class="k0 gap"><title>union(all but detect-secrets): credential-shaped, 3.92 per MB</title></rect>
|
|
96
|
+
<rect x="287.23726039221594" y="355" width="50.134423243708056" height="16" class="k1 gap"><title>union(all but detect-secrets): hex, 7.02 per MB</title></rect>
|
|
97
|
+
<rect x="337.371683635924" y="355" width="103.95170696266479" height="16" class="k2 gap"><title>union(all but detect-secrets): word or placeholder, 14.55 per MB</title></rect>
|
|
98
|
+
<path d="M441.3233905985888,355 h20.59200381859614 a4,4 0 0 1 4,4 v8 a4,4 0 0 1 -4,4 h-20.59200381859614 z" class="k3 gap"><title>union(all but detect-secrets): other, 3.44 per MB</title></path>
|
|
99
|
+
<text class="tick" x="471.9153944171849" y="367">29</text>
|
|
100
|
+
<text x="249.20000000000005" y="393" text-anchor="end">all seven combined</text>
|
|
101
|
+
<rect x="259.20000000000005" y="381" width="39.79865352284878" height="16" class="k0 gap"><title>union(all 7): credential-shaped, 5.57 per MB</title></rect>
|
|
102
|
+
<rect x="298.9986535228488" y="381" width="88.7450572584122" height="16" class="k1 gap"><title>union(all 7): hex, 12.42 per MB</title></rect>
|
|
103
|
+
<rect x="387.74371078126103" y="381" width="108.22857719198583" height="16" class="k2 gap"><title>union(all 7): word or placeholder, 15.14 per MB</title></rect>
|
|
104
|
+
<path d="M495.97228797324686,381 h126.56334394510696 a4,4 0 0 1 4,4 v8 a4,4 0 0 1 -4,4 h-126.56334394510696 z" class="k3 gap"><title>union(all 7): other, 18.27 per MB</title></path>
|
|
105
|
+
<text class="tick" x="632.5356319183538" y="393">51</text>
|
|
106
|
+
<line class="axis" x1="259.20000000000005" x2="259.20000000000005" y1="60" y2="402"/>
|
|
107
|
+
<rect x="538.3199999999999" y="68" width="12" height="12" rx="3" class="k0"/><text x="556.3199999999999" y="78">credential-shaped</text>
|
|
108
|
+
<rect x="538.3199999999999" y="88" width="12" height="12" rx="3" class="k1"/><text x="556.3199999999999" y="98">hex</text>
|
|
109
|
+
<rect x="538.3199999999999" y="108" width="12" height="12" rx="3" class="k2"/><text x="556.3199999999999" y="118">word or placeholder</text>
|
|
110
|
+
<rect x="538.3199999999999" y="128" width="12" height="12" rx="3" class="k3"/><text x="556.3199999999999" y="138">other</text>
|
|
111
|
+
<text x="556.3199999999999" y="158" class="ours">bold: redac---n</text>
|
|
112
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "redac---n",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Redact secrets from text and agent transcripts before they reach an untrusted model provider",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"secrets",
|
|
7
|
+
"redaction",
|
|
8
|
+
"redact",
|
|
9
|
+
"credentials",
|
|
10
|
+
"llm",
|
|
11
|
+
"agents",
|
|
12
|
+
"transcripts",
|
|
13
|
+
"dlp"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/KTibow/redac---n.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/KTibow/redac---n#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/KTibow/redac---n/issues"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"docs",
|
|
34
|
+
"THIRD_PARTY_NOTICES.md"
|
|
35
|
+
],
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
|
|
42
|
+
"prepack": "pnpm run build",
|
|
43
|
+
"typecheck": "tsc -p . && tsc -p tsconfig.build.json --noEmit",
|
|
44
|
+
"test": "node --test test/*.test.ts",
|
|
45
|
+
"eval": "node eval/all.ts",
|
|
46
|
+
"check": "node eval/check.ts",
|
|
47
|
+
"site": "node scripts/build-site.ts",
|
|
48
|
+
"format": "prettier --write .",
|
|
49
|
+
"lint": "prettier --check . && knip"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^26.6.4",
|
|
53
|
+
"knip": "^6.41.0",
|
|
54
|
+
"prettier": "^3.9.10",
|
|
55
|
+
"rolldown": "^1.2.13",
|
|
56
|
+
"typescript": "^7.0.2"
|
|
57
|
+
}
|
|
58
|
+
}
|