leakferret 0.2.0-x86_64-linux
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.
- checksums.yaml +7 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +250 -0
- data/exe/leakferret +8 -0
- data/lib/leakferret/bin/leakferret +0 -0
- data/lib/leakferret/binary.rb +90 -0
- data/lib/leakferret/client.rb +87 -0
- data/lib/leakferret/error.rb +29 -0
- data/lib/leakferret/platform.rb +52 -0
- data/lib/leakferret/version.rb +14 -0
- data/lib/leakferret.rb +102 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bd9375c03897c6ce0d46a768d62356484dc54365310952d6a1e2afccd31ccb26
|
|
4
|
+
data.tar.gz: 9dafdf0b3de2cdf0184d2c4a8d0373084435347e87cb078d9145217e32b5e24e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b5294f97cb9d81711cd756224a3bf7b3b4e2c8f9eac1406b7c40374c174cbde2c061cee23f9b67e0da52053626510d60bdc6912a4cc9f439a4158e56a2bbfc49
|
|
7
|
+
data.tar.gz: 3c4ca608c5d66f8319d3827bd224cd724150cd745f18916e565d8f3e7edc06d251a4be88e81bc07f86fafb2ee35f7279b75edb1c51343670e96d03e0c9bb683f
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `leakferret` gem are documented here. The gem version
|
|
4
|
+
can move independently of the native binary version it targets.
|
|
5
|
+
|
|
6
|
+
## [0.2.0] - 2026-06-04
|
|
7
|
+
|
|
8
|
+
Minor bump (not patch): this removes the binary-download fallback, so a
|
|
9
|
+
previously-supported install path is gone on platforms without a prebuilt gem
|
|
10
|
+
(notably arm64-Windows). Breaking change under the 0.x convention.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Ship **precompiled platform gems** (x86_64-linux, x86_64-darwin, arm64-darwin,
|
|
14
|
+
x64-mingw-ucrt) with the native binary bundled inside the gem. A normal
|
|
15
|
+
`gem install` gets the binary through RubyGems itself: no download, no
|
|
16
|
+
network, no Rust toolchain. Audit it with `gem unpack leakferret`. Mirrors how
|
|
17
|
+
`nokogiri` and `sorbet-static` ship.
|
|
18
|
+
- **Removed the binary-download flow entirely.** Dropped the install-time
|
|
19
|
+
`extconf.rb` pre-fetch and the runtime download + SHA256 logic. The gem now
|
|
20
|
+
has no fetch-and-run code to audit at all.
|
|
21
|
+
- On a platform without a prebuilt binary (e.g. aarch64-linux, Alpine/musl), the
|
|
22
|
+
source gem still installs (so `bundle install` resolves) and raises a clear
|
|
23
|
+
message pointing to `cargo install leakferret-cli` or `LEAKFERRET_BIN`. It
|
|
24
|
+
never downloads.
|
|
25
|
+
- Released gems now carry GitHub build provenance (SLSA attestation).
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
- `ext/leakferret/extconf.rb` and the runtime binary downloader.
|
|
29
|
+
|
|
30
|
+
Targets the leakferret `0.1.9` binary (unchanged).
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maria Khan
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/leakferrethq/leakferret/master/brand/logo.png" alt="leakferret" width="380">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# leakferret (Ruby gem)
|
|
6
|
+
|
|
7
|
+
> MCP-native secret scanner — verified findings, agent-applied rewrites.
|
|
8
|
+
|
|
9
|
+
[](https://rubygems.org/gems/leakferret)
|
|
10
|
+
[](https://github.com/leakferrethq/leakferret/blob/master/LICENSE.txt)
|
|
11
|
+
[](https://lobehub.com/mcp/leakferrethq-leakferret-ruby)
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<img src="https://raw.githubusercontent.com/leakferrethq/leakferret/master/brand/demo.gif" alt="leakferret finds, verifies, and rewrites a leaked secret" width="760">
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
Ruby gem wrapper around the native [`leakferret`](https://github.com/leakferrethq/leakferret)
|
|
18
|
+
binary. This gem ships no scanning logic of its own: it installs a tiny Ruby
|
|
19
|
+
shim plus the prebuilt, statically-linked engine (written in Rust). All the
|
|
20
|
+
work — scan, classify, verify, rewrite — happens in that single binary.
|
|
21
|
+
|
|
22
|
+
You get a **precompiled platform gem** with the binary bundled inside it, the
|
|
23
|
+
same way `nokogiri` and `sorbet-static` ship their native code, for x86_64
|
|
24
|
+
Linux (glibc), macOS (x86_64 and arm64), and x86_64 Windows. The binary travels
|
|
25
|
+
through RubyGems with the gem — **no download, no network access, no Rust
|
|
26
|
+
toolchain** — and you can audit exactly what you will run with
|
|
27
|
+
`gem unpack leakferret`. The gem has **no fetch-and-run code at all**. On any
|
|
28
|
+
other platform the source gem installs and points you at a one-line fix (build
|
|
29
|
+
from source, or set `LEAKFERRET_BIN`); it never downloads a binary.
|
|
30
|
+
|
|
31
|
+
## What leakferret does
|
|
32
|
+
|
|
33
|
+
leakferret finds hardcoded secrets and API keys in your code and helps you
|
|
34
|
+
remove them, in five stations:
|
|
35
|
+
|
|
36
|
+
1. **Scan** — regex pre-filter over files; respects `.gitignore` and also reads
|
|
37
|
+
dotfiles like `.env`.
|
|
38
|
+
2. **Catalog** — a signed database of known-public example credentials (Stripe
|
|
39
|
+
test keys, `AKIAIOSFODNN7EXAMPLE`, jwt.io samples) so documented examples are
|
|
40
|
+
marked `FIXTURE` instead of false-alarming.
|
|
41
|
+
3. **Classify** — a `REAL` / `FIXTURE` / `UNKNOWN` verdict, from offline
|
|
42
|
+
heuristics or by asking the host editor/agent language model (no extra API
|
|
43
|
+
key, no cost).
|
|
44
|
+
4. **Verify** — a real but harmless API call to the provider (AWS SigV4,
|
|
45
|
+
GitHub, GitLab, Stripe, OpenAI, Anthropic, Slack, Twilio, SendGrid, Mailgun,
|
|
46
|
+
Datadog, Heroku, npm, PyPI, DigitalOcean) to confirm a key is live, plus a
|
|
47
|
+
trufflehog fallback.
|
|
48
|
+
5. **Rewrite** — swap a hardcoded literal for an environment-variable lookup
|
|
49
|
+
(`ENV.fetch` in Ruby, `process.env` in JS, `os.environ` in Python), add a
|
|
50
|
+
`.env.example` line, and print secret-manager seed commands.
|
|
51
|
+
|
|
52
|
+
**Privacy invariant:** the full secret value never leaves your machine. Only a
|
|
53
|
+
redacted first-4-plus-last-4 preview (e.g. `AKIA...4XYZ`) is ever written to a
|
|
54
|
+
report, log, network message, or model prompt. Verification calls go straight
|
|
55
|
+
from your machine to the provider — leakferret has no servers.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
gem install leakferret
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
RubyGems and Bundler select the right precompiled gem for your platform
|
|
64
|
+
automatically, so the binary is already there after `gem install` — no
|
|
65
|
+
first-run download. Inspect it before you trust it:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
gem unpack leakferret # the bundled binary is at lib/leakferret/bin/
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Released gems carry GitHub build provenance (SLSA), so you can verify each gem
|
|
72
|
+
was built from the tagged source in CI. On a platform without a prebuilt binary
|
|
73
|
+
(e.g. aarch64-linux or Alpine/musl), the source gem still installs — so
|
|
74
|
+
`bundle install` resolves — and tells you to build from source
|
|
75
|
+
(`cargo install leakferret-cli`) or set `LEAKFERRET_BIN`. It never downloads.
|
|
76
|
+
|
|
77
|
+
Add it to a `Gemfile` for project-local use:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
gem 'leakferret'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Requires Ruby >= 3.1.
|
|
84
|
+
|
|
85
|
+
## CLI
|
|
86
|
+
|
|
87
|
+
The gem installs a `leakferret` executable that simply `exec`s the binary, so
|
|
88
|
+
every subcommand and flag works exactly as upstream:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
leakferret scan .
|
|
92
|
+
leakferret verify . --only-verified
|
|
93
|
+
leakferret rewrite . --apply --backend doppler
|
|
94
|
+
leakferret baseline init
|
|
95
|
+
leakferret catalog info
|
|
96
|
+
leakferret mcp # MCP server on stdio
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`leakferret scan --git` walks commit history. Output formats are `pretty`
|
|
100
|
+
(colored terminal), `json`, and `sarif` (for GitHub Code Scanning).
|
|
101
|
+
|
|
102
|
+
## Ruby API
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
require 'leakferret'
|
|
106
|
+
|
|
107
|
+
# Regex pre-filter only.
|
|
108
|
+
findings = Leakferret.scan('.')
|
|
109
|
+
|
|
110
|
+
# + provider-verified (live HTTP to GitHub / Stripe / AWS / ...).
|
|
111
|
+
findings = Leakferret.verify('.', mode: 'only-verified')
|
|
112
|
+
|
|
113
|
+
# + propose rewrites for REAL findings.
|
|
114
|
+
findings = Leakferret.rewrite('.', backend: 'doppler')
|
|
115
|
+
|
|
116
|
+
# Apply rewrites in place.
|
|
117
|
+
Leakferret.rewrite('.', apply: true)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Each `Finding` is a hash with `path`, `line`, `column`, `pattern`, `severity`
|
|
121
|
+
(`critical`/`high`/`medium`/`low`), `verdict` (`real`/`fixture`/`unknown`),
|
|
122
|
+
`match_redacted`, `confidence`, `verification`, and `fingerprint`.
|
|
123
|
+
|
|
124
|
+
## Rewrite a leak
|
|
125
|
+
|
|
126
|
+
`rewrite` turns a hardcoded secret into an env-var lookup and helps you move it
|
|
127
|
+
into a secret manager:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
leakferret rewrite . --dry-run-diff # preview the change, touch nothing
|
|
131
|
+
leakferret rewrite . --apply # write `ENV.fetch("KEY")` in place + add to .env.example
|
|
132
|
+
leakferret rewrite . --apply --backend doppler # also print seed commands for your manager
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`--backend` accepts `env` (default), `vault`, `doppler`, `aws-secrets-manager`,
|
|
136
|
+
`infisical`. By default it only rewrites findings confirmed **REAL/live**; add
|
|
137
|
+
`--include-unknown` to also fix unconfirmed candidates.
|
|
138
|
+
|
|
139
|
+
## Use it in CI
|
|
140
|
+
|
|
141
|
+
leakferret is one binary with clear exit codes (`0` = clean, `1` = findings), so
|
|
142
|
+
it drops into any CI. The recommended pattern: **baseline once**, then `verify`
|
|
143
|
+
on every build so you only fail on *new* secrets.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# One-time, on a repo that may already have findings:
|
|
147
|
+
leakferret baseline init # fingerprints current findings (HMAC, never the raw secret)
|
|
148
|
+
git add .leakferret-baseline.json # commit it — the per-repo salt is auto-gitignored
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
After that, `verify` ignores anything in the baseline and fails only on new leaks.
|
|
152
|
+
|
|
153
|
+
**GitHub Actions** — use the dedicated action (uploads SARIF to Code Scanning):
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
- uses: leakferrethq/leakferret-action@v1
|
|
157
|
+
with: { path: ., fail-on: any }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**CircleCI:**
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
jobs:
|
|
164
|
+
secrets:
|
|
165
|
+
docker: [{ image: cimg/ruby:3.3 }]
|
|
166
|
+
steps:
|
|
167
|
+
- checkout
|
|
168
|
+
- run: gem install leakferret
|
|
169
|
+
- run: leakferret verify . --format sarif > leakferret.sarif
|
|
170
|
+
- store_artifacts: { path: leakferret.sarif }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**GitLab CI / Argo Workflows / Jenkins / anything else** — identical recipe:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
gem install leakferret
|
|
177
|
+
leakferret verify . # exits 1 on any REAL finding -> fails the job
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Useful flags: `--only-verified` (fail only on provider-confirmed live keys),
|
|
181
|
+
`--verify-mode ever-verified` (with a baseline, fail on anything that *ever*
|
|
182
|
+
verified live), `--format sarif|json`.
|
|
183
|
+
|
|
184
|
+
## Use it with AI agents (MCP)
|
|
185
|
+
|
|
186
|
+
leakferret is also an MCP server, so a coding agent (Cursor, Claude, Continue)
|
|
187
|
+
can scan, verify, and rewrite *before it commits*. Add it to your editor's MCP
|
|
188
|
+
config:
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"mcpServers": {
|
|
193
|
+
"leakferret": { "command": "leakferret", "args": ["mcp"] }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
In **Cursor**: Settings → MCP → Add. In **Claude Desktop**: the `mcpServers`
|
|
199
|
+
block of `claude_desktop_config.json`. Tools exposed: `scan_repository`,
|
|
200
|
+
`classify_candidates`, `verify_finding`, `propose_rewrite`, `baseline_diff`.
|
|
201
|
+
|
|
202
|
+
> Running `leakferret mcp` directly in a terminal looks like it hangs — that's
|
|
203
|
+
> correct. It's a stdio JSON-RPC server waiting for your editor to connect, not
|
|
204
|
+
> a command you run by hand.
|
|
205
|
+
|
|
206
|
+
## Using a local binary
|
|
207
|
+
|
|
208
|
+
Every leakferret wrapper honors the `LEAKFERRET_BIN` environment variable. Point
|
|
209
|
+
it at a binary on disk and the wrapper runs that instead of the downloaded copy:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
export LEAKFERRET_BIN=/opt/leakferret/leakferret
|
|
213
|
+
leakferret scan .
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
For air-gapped or offline installs, set `LEAKFERRET_SKIP_DOWNLOAD=1` to skip the
|
|
217
|
+
release download and position the binary yourself.
|
|
218
|
+
|
|
219
|
+
## Block commits locally (pre-commit hook)
|
|
220
|
+
|
|
221
|
+
Catch a secret before it is ever committed. From your repo root:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
cat > .git/hooks/pre-commit <<'HOOK'
|
|
225
|
+
#!/bin/sh
|
|
226
|
+
# Offline secret scan (no network). Blocks the commit on any finding.
|
|
227
|
+
leakferret verify . --verify-mode none --fail-on any || {
|
|
228
|
+
echo "leakferret blocked this commit. Bypass: git commit --no-verify"
|
|
229
|
+
exit 1
|
|
230
|
+
}
|
|
231
|
+
HOOK
|
|
232
|
+
chmod +x .git/hooks/pre-commit
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`--verify-mode none` keeps it offline; `--fail-on any` exits non-zero on any
|
|
236
|
+
non-fixture finding (documented examples like `AKIAIOSFODNN7EXAMPLE` are still
|
|
237
|
+
ignored). Pair with `leakferret baseline init` to block only on *new* secrets,
|
|
238
|
+
or commit the hook to `.githooks/` and run `git config core.hooksPath .githooks`
|
|
239
|
+
to share it with a team.
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT for this gem and the bundled binary. The fixture catalog **data** is
|
|
244
|
+
CC-BY-SA-4.0 — see [`leakferret-catalog`](https://github.com/leakferrethq/leakferret-catalog).
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
Part of [leakferret](https://github.com/leakferrethq/leakferret) ·
|
|
249
|
+
[leakferret.com](https://leakferret.com) ·
|
|
250
|
+
maintained by Maria Khan <missusk@protonmail.com>.
|
data/exe/leakferret
ADDED
|
Binary file
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
require_relative 'version'
|
|
6
|
+
require_relative 'platform'
|
|
7
|
+
require_relative 'error'
|
|
8
|
+
|
|
9
|
+
module Leakferret
|
|
10
|
+
# Resolves the native `leakferret` binary that ships inside the gem.
|
|
11
|
+
#
|
|
12
|
+
# Precompiled platform gems bundle the binary at lib/leakferret/bin/. There is
|
|
13
|
+
# deliberately no download path: the gem either carries the binary for your
|
|
14
|
+
# platform or it tells you how to provide one. Nothing here touches the
|
|
15
|
+
# network, so what you `gem unpack` is exactly what runs - the whole gem is
|
|
16
|
+
# auditable, with no fetch-and-execute code to vet.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
module Binary
|
|
20
|
+
# Where a precompiled platform gem stages the native binary.
|
|
21
|
+
BUNDLED_DIR = Pathname.new(__dir__).join('bin').freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# Absolute path to the native binary. Resolution order:
|
|
26
|
+
# 1. LEAKFERRET_BIN - explicit override
|
|
27
|
+
# 2. lib/leakferret/bin/<name> - bundled by the precompiled platform gem
|
|
28
|
+
#
|
|
29
|
+
# @return [String] absolute path to the executable
|
|
30
|
+
# @raise [BinaryNotFoundError] if the override is missing, or no binary is
|
|
31
|
+
# bundled for this platform
|
|
32
|
+
def path
|
|
33
|
+
override = ENV['LEAKFERRET_BIN']
|
|
34
|
+
unless override.nil? || override.empty?
|
|
35
|
+
unless File.file?(override)
|
|
36
|
+
raise BinaryNotFoundError, "LEAKFERRET_BIN points to a missing file: #{override}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
return override
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
bundled = BUNDLED_DIR.join(Platform.binary_name)
|
|
43
|
+
if bundled.file?
|
|
44
|
+
# Make sure it is executable in case the mode did not survive packaging
|
|
45
|
+
# or install; the gem dir is usually writable, a read-only one is
|
|
46
|
+
# harmless to skip.
|
|
47
|
+
unless Platform.windows? || bundled.executable?
|
|
48
|
+
begin
|
|
49
|
+
bundled.chmod(0o755)
|
|
50
|
+
rescue StandardError
|
|
51
|
+
# best effort
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
return bundled.to_s
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
raise BinaryNotFoundError, no_binary_message
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Message shown when the source/fallback gem is installed on a platform with
|
|
61
|
+
# no precompiled binary. No automatic download is attempted - the user
|
|
62
|
+
# provides the binary, or builds it.
|
|
63
|
+
#
|
|
64
|
+
# @return [String] multi-line build-from-source instructions
|
|
65
|
+
def no_binary_message
|
|
66
|
+
plat =
|
|
67
|
+
begin
|
|
68
|
+
Gem::Platform.local.to_s
|
|
69
|
+
rescue StandardError
|
|
70
|
+
RUBY_PLATFORM
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
<<~MSG
|
|
74
|
+
No prebuilt leakferret binary ships for this platform (#{plat}).
|
|
75
|
+
|
|
76
|
+
leakferret publishes precompiled gems for x86_64 and arm64 macOS,
|
|
77
|
+
x86_64 Linux (glibc), and x86_64 Windows. On any other platform, provide
|
|
78
|
+
the binary yourself - either of these works:
|
|
79
|
+
|
|
80
|
+
1. Build from source and point LEAKFERRET_BIN at it:
|
|
81
|
+
cargo install leakferret-cli
|
|
82
|
+
export LEAKFERRET_BIN="$(command -v leakferret)"
|
|
83
|
+
|
|
84
|
+
2. Download a release binary for your platform from
|
|
85
|
+
https://github.com/leakferrethq/leakferret/releases
|
|
86
|
+
and set LEAKFERRET_BIN to its absolute path.
|
|
87
|
+
MSG
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
module Leakferret
|
|
7
|
+
# Thin shell-out wrapper around the native binary. Each public method invokes
|
|
8
|
+
# `leakferret <verb> --format json` and parses the resulting array. You
|
|
9
|
+
# normally call the module-level {Leakferret.scan}, {Leakferret.verify}, and
|
|
10
|
+
# {Leakferret.rewrite} helpers instead of constructing this directly.
|
|
11
|
+
class Client
|
|
12
|
+
# Run a scan-only pass (regex pre-filter, offline).
|
|
13
|
+
#
|
|
14
|
+
# @param path [String] file or directory to scan
|
|
15
|
+
# @param exclude [Array<String>] glob(s) to skip
|
|
16
|
+
# @param only [Array<String>, String, nil] restrict the scan to these path(s)
|
|
17
|
+
# @param show_fixtures [Boolean] include catalog fixtures in the result
|
|
18
|
+
# @return [Array<Hash>] candidate finding hashes
|
|
19
|
+
# @raise [BinaryInvocationError] on an unexpected exit status
|
|
20
|
+
def scan(path, exclude: [], only: nil, show_fixtures: false)
|
|
21
|
+
run(['scan', path, '--format', 'json'] + format_flags(exclude:, only:, show_fixtures:))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Run scan + classify + provider verification.
|
|
25
|
+
#
|
|
26
|
+
# @param path [String] file or directory to scan
|
|
27
|
+
# @param mode [String] verify mode passed to `--verify-mode`
|
|
28
|
+
# @param timeout [Integer] per-verifier timeout in seconds
|
|
29
|
+
# @option opts [Array<String>] :exclude glob(s) to skip
|
|
30
|
+
# @option opts [Array<String>, String] :only restrict the scan to these path(s)
|
|
31
|
+
# @option opts [Boolean] :show_fixtures include catalog fixtures
|
|
32
|
+
# @return [Array<Hash>] findings with verification and verdict filled in
|
|
33
|
+
# @raise [BinaryInvocationError] on an unexpected exit status
|
|
34
|
+
def verify(path, mode: 'best-effort', timeout: 10, **opts)
|
|
35
|
+
run(['verify', path, '--format', 'json', '--verify-mode', mode,
|
|
36
|
+
'--verifier-timeout-secs', timeout.to_s] + format_flags(**opts))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Run scan + classify + rewrite proposal.
|
|
40
|
+
#
|
|
41
|
+
# @param path [String] file or directory to scan
|
|
42
|
+
# @param apply [Boolean] write the rewrites in place when true
|
|
43
|
+
# @param backend [String] rewrite backend (e.g. `env`, `doppler`)
|
|
44
|
+
# @option opts [Array<String>] :exclude glob(s) to skip
|
|
45
|
+
# @option opts [Array<String>, String] :only restrict the scan to these path(s)
|
|
46
|
+
# @return [Array<Hash>] findings, each with a proposed replacement
|
|
47
|
+
# @raise [BinaryInvocationError] on an unexpected exit status
|
|
48
|
+
def rewrite(path, apply: false, backend: 'env', **opts)
|
|
49
|
+
args = ['rewrite', path, '--format', 'json', '--backend', backend]
|
|
50
|
+
args << '--apply' if apply
|
|
51
|
+
run(args + format_flags(**opts))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Build the shared `--exclude` / `--only` / `--show-fixtures` flag list.
|
|
57
|
+
#
|
|
58
|
+
# @return [Array<String>] CLI flags
|
|
59
|
+
# @api private
|
|
60
|
+
def format_flags(exclude: [], only: nil, show_fixtures: false)
|
|
61
|
+
flags = []
|
|
62
|
+
Array(exclude).each { |g| flags.push('--exclude', g) }
|
|
63
|
+
Array(only).each { |p| flags.push('--only', p) }
|
|
64
|
+
flags << '--show-fixtures' if show_fixtures
|
|
65
|
+
flags
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Invoke the binary and parse its JSON output. Exit codes 0 (clean) and 1
|
|
69
|
+
# (findings present) are both treated as success; any other status raises.
|
|
70
|
+
#
|
|
71
|
+
# @param args [Array<String>] argv passed to the binary
|
|
72
|
+
# @return [Array<Hash>] the parsed findings (empty array when output is blank)
|
|
73
|
+
# @raise [BinaryInvocationError] if the binary exits with a status other than 0 or 1
|
|
74
|
+
# @api private
|
|
75
|
+
def run(args)
|
|
76
|
+
out, err, status = Open3.capture3(Binary.path, *args)
|
|
77
|
+
unless [0, 1].include?(status.exitstatus)
|
|
78
|
+
raise BinaryInvocationError.new(
|
|
79
|
+
"leakferret exited with #{status.exitstatus}",
|
|
80
|
+
exit_status: status.exitstatus,
|
|
81
|
+
stderr: err,
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
JSON.parse(out.strip.empty? ? '[]' : out)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Leakferret
|
|
4
|
+
# Base class for every error this gem raises. Rescue this to catch them all.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when the native binary cannot be located or downloaded (no pinned
|
|
8
|
+
# checksum for the platform, a failed download, or a bad `LEAKFERRET_BIN`).
|
|
9
|
+
class BinaryNotFoundError < Error; end
|
|
10
|
+
|
|
11
|
+
# Raised when the binary runs but exits with an unexpected status (anything
|
|
12
|
+
# other than 0 for a clean run or 1 for findings present).
|
|
13
|
+
class BinaryInvocationError < Error
|
|
14
|
+
# @return [Integer] the binary's process exit status
|
|
15
|
+
attr_reader :exit_status
|
|
16
|
+
|
|
17
|
+
# @return [String] captured standard error from the failed invocation
|
|
18
|
+
attr_reader :stderr
|
|
19
|
+
|
|
20
|
+
# @param message [String] human-readable error message
|
|
21
|
+
# @param exit_status [Integer] the binary's exit status
|
|
22
|
+
# @param stderr [String] captured standard error
|
|
23
|
+
def initialize(message, exit_status:, stderr:)
|
|
24
|
+
super(message)
|
|
25
|
+
@exit_status = exit_status
|
|
26
|
+
@stderr = stderr
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
|
|
5
|
+
require_relative 'error'
|
|
6
|
+
|
|
7
|
+
module Leakferret
|
|
8
|
+
# Host detection for picking the right release asset. Maps the running
|
|
9
|
+
# Ruby's `RbConfig` host to a Rust target triple and binary name.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module Platform
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# The Rust target triple for the current host (e.g.
|
|
16
|
+
# `x86_64-unknown-linux-gnu`).
|
|
17
|
+
#
|
|
18
|
+
# @return [String] the target triple
|
|
19
|
+
# @raise [Error] on an unsupported CPU or OS, or aarch64-linux (no asset yet)
|
|
20
|
+
def triple
|
|
21
|
+
cpu = case RbConfig::CONFIG['host_cpu']
|
|
22
|
+
when /x86_64|amd64|x64/ then 'x86_64'
|
|
23
|
+
when /aarch64|arm64/ then 'aarch64'
|
|
24
|
+
else
|
|
25
|
+
raise Error, "unsupported CPU: #{RbConfig::CONFIG['host_cpu']}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
case RbConfig::CONFIG['host_os']
|
|
29
|
+
when /mswin|mingw|cygwin/ then "#{cpu}-pc-windows-msvc"
|
|
30
|
+
when /darwin/ then "#{cpu}-apple-darwin"
|
|
31
|
+
when /linux/
|
|
32
|
+
# No aarch64-linux release asset yet (v0.1.0 ships x86_64 only).
|
|
33
|
+
raise Error, 'aarch64-linux has no prebuilt binary yet; build from source' if cpu == 'aarch64'
|
|
34
|
+
|
|
35
|
+
"#{cpu}-unknown-linux-gnu"
|
|
36
|
+
else
|
|
37
|
+
raise Error, "unsupported OS: #{RbConfig::CONFIG['host_os']}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Executable name for the current OS.
|
|
42
|
+
# @return [String] `"leakferret.exe"` on Windows, otherwise `"leakferret"`
|
|
43
|
+
def binary_name
|
|
44
|
+
windows? ? 'leakferret.exe' : 'leakferret'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Integer, nil] truthy when the host OS is Windows
|
|
48
|
+
def windows?
|
|
49
|
+
RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Leakferret
|
|
4
|
+
# The gem's own version (what `gem install leakferret` resolves).
|
|
5
|
+
# @return [String]
|
|
6
|
+
VERSION = '0.2.0'
|
|
7
|
+
|
|
8
|
+
# The native binary release this gem bundles and runs. The release workflow
|
|
9
|
+
# stages this version's binary into each precompiled platform gem. Tracks the
|
|
10
|
+
# leakferret core release and can move independently of {VERSION} (e.g. a
|
|
11
|
+
# gem-only bugfix keeps the same binary).
|
|
12
|
+
# @return [String]
|
|
13
|
+
BINARY_VERSION = '0.1.9'
|
|
14
|
+
end
|
data/lib/leakferret.rb
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
require 'leakferret/version'
|
|
7
|
+
require 'leakferret/error'
|
|
8
|
+
require 'leakferret/platform'
|
|
9
|
+
require 'leakferret/binary'
|
|
10
|
+
require 'leakferret/client'
|
|
11
|
+
|
|
12
|
+
# Ruby wrapper around the native `leakferret` secret scanner.
|
|
13
|
+
#
|
|
14
|
+
# leakferret finds hardcoded secrets, confirms which ones are actually live by
|
|
15
|
+
# calling the provider, and rewrites them to read from environment variables.
|
|
16
|
+
# This gem is a thin wrapper: the native binary (written in Rust) is downloaded
|
|
17
|
+
# once per platform on first use and cached, then each call shells out to it
|
|
18
|
+
# and parses the JSON it prints. The full secret value never leaves your
|
|
19
|
+
# machine; every finding carries only a redacted `first4...last4` preview.
|
|
20
|
+
#
|
|
21
|
+
# The three top-level methods mirror the CLI verbs and each return an array of
|
|
22
|
+
# finding hashes.
|
|
23
|
+
#
|
|
24
|
+
# @example Scan the working tree
|
|
25
|
+
# Leakferret.scan(".").each do |f|
|
|
26
|
+
# puts "#{f['path']}:#{f['line']} #{f['pattern']} [#{f['verdict']}]"
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @example Fail a script on any live secret
|
|
30
|
+
# exit(1) unless Leakferret.verify(".", mode: "only-verified").empty?
|
|
31
|
+
#
|
|
32
|
+
# @see https://leakferret.com
|
|
33
|
+
# @see https://github.com/leakferrethq/leakferret
|
|
34
|
+
module Leakferret
|
|
35
|
+
class << self
|
|
36
|
+
# Scan a path for candidate secrets. This is the regex pre-filter only (no
|
|
37
|
+
# classification, no verification): the fastest, fully offline pass.
|
|
38
|
+
#
|
|
39
|
+
# @param path [String] file or directory to scan, relative or absolute
|
|
40
|
+
# @option opts [Array<String>] :exclude glob(s) to skip
|
|
41
|
+
# @option opts [Array<String>, String] :only restrict the scan to these path(s)
|
|
42
|
+
# @option opts [Boolean] :show_fixtures (false) include catalog fixtures in the result
|
|
43
|
+
# @return [Array<Hash>] candidate findings, each with `path`, `line`,
|
|
44
|
+
# `pattern`, `verdict`, and `match_redacted` keys
|
|
45
|
+
# @raise [BinaryInvocationError] if the binary exits with an unexpected status
|
|
46
|
+
# @example
|
|
47
|
+
# Leakferret.scan("app/", exclude: ["**/*_test.rb"])
|
|
48
|
+
def scan(path = '.', **opts)
|
|
49
|
+
Client.new.scan(path, **opts)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Scan, classify, and verify. Real findings are confirmed live with a
|
|
53
|
+
# harmless API call to the provider (AWS, GitHub, Stripe, and others), so
|
|
54
|
+
# this method makes outbound network requests.
|
|
55
|
+
#
|
|
56
|
+
# @param path [String] file or directory to scan
|
|
57
|
+
# @option opts [String] :mode ("best-effort") verify mode: `none`,
|
|
58
|
+
# `best-effort`, `only-verified`, or `ever-verified`
|
|
59
|
+
# @option opts [Integer] :timeout (10) per-verifier timeout in seconds
|
|
60
|
+
# @option opts [Array<String>] :exclude glob(s) to skip
|
|
61
|
+
# @option opts [Array<String>, String] :only restrict the scan to these path(s)
|
|
62
|
+
# @return [Array<Hash>] findings with `verdict` and `verification` filled in
|
|
63
|
+
# @raise [BinaryInvocationError] if the binary exits with an unexpected status
|
|
64
|
+
# @example Only return secrets confirmed live
|
|
65
|
+
# Leakferret.verify(".", mode: "only-verified")
|
|
66
|
+
def verify(path = '.', **opts)
|
|
67
|
+
Client.new.verify(path, **opts)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Scan, classify, and propose environment-variable rewrites for real
|
|
71
|
+
# findings. Pass `apply: true` to write the rewrites to disk in place.
|
|
72
|
+
#
|
|
73
|
+
# @param path [String] file or directory to scan
|
|
74
|
+
# @param apply [Boolean] when true, edit files in place; otherwise only
|
|
75
|
+
# propose the replacements
|
|
76
|
+
# @option opts [String] :backend ("env") rewrite backend, e.g. `env`, `doppler`
|
|
77
|
+
# @return [Array<Hash>] findings, each with a `replacement` proposal attached
|
|
78
|
+
# @raise [BinaryInvocationError] if the binary exits with an unexpected status
|
|
79
|
+
# @example Apply rewrites and seed Doppler
|
|
80
|
+
# Leakferret.rewrite(".", apply: true, backend: "doppler")
|
|
81
|
+
def rewrite(path = '.', apply: false, **opts)
|
|
82
|
+
Client.new.rewrite(path, apply: apply, **opts)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Absolute path to the native binary, downloading it on first use.
|
|
86
|
+
#
|
|
87
|
+
# @return [String] absolute filesystem path to the `leakferret` executable
|
|
88
|
+
# @raise [BinaryNotFoundError] if the binary is missing and cannot be fetched
|
|
89
|
+
def binary_path
|
|
90
|
+
Binary.path
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Version string reported by the bundled native binary. May differ from
|
|
94
|
+
# {VERSION} (the gem's own version) during pre-release; see {BINARY_VERSION}.
|
|
95
|
+
#
|
|
96
|
+
# @return [String] the binary's `--version` output, stripped
|
|
97
|
+
def binary_version
|
|
98
|
+
out, _err, _status = Open3.capture3(binary_path, '--version')
|
|
99
|
+
out.strip
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: leakferret
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: x86_64-linux
|
|
6
|
+
authors:
|
|
7
|
+
- Maria Khan
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |
|
|
14
|
+
Context-aware secret scanning for Ruby projects. A thin wrapper around the
|
|
15
|
+
native leakferret binary (written in Rust): it finds hardcoded secrets,
|
|
16
|
+
confirms which ones are actually live by calling the provider, and rewrites
|
|
17
|
+
them to read from environment variables instead.
|
|
18
|
+
|
|
19
|
+
Precompiled platform gems bundle the native binary inside the gem, so a
|
|
20
|
+
normal `gem install` ships the binary through RubyGems itself: no download,
|
|
21
|
+
no network access, and no Rust toolchain. You can audit exactly what you are
|
|
22
|
+
about to run with `gem unpack leakferret`. The gem never fetches and runs a
|
|
23
|
+
binary off the internet - there is no download code to vet. On a platform
|
|
24
|
+
without a prebuilt gem, the source gem tells you to build from source
|
|
25
|
+
(`cargo install leakferret-cli`) or point LEAKFERRET_BIN at a binary.
|
|
26
|
+
|
|
27
|
+
The API exposes Leakferret.scan, Leakferret.verify, and Leakferret.rewrite
|
|
28
|
+
(each returning Finding objects), plus a `leakferret` command-line tool.
|
|
29
|
+
email:
|
|
30
|
+
- missusk@protonmail.com
|
|
31
|
+
executables:
|
|
32
|
+
- leakferret
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- ".yardopts"
|
|
37
|
+
- CHANGELOG.md
|
|
38
|
+
- LICENSE.txt
|
|
39
|
+
- README.md
|
|
40
|
+
- exe/leakferret
|
|
41
|
+
- lib/leakferret.rb
|
|
42
|
+
- lib/leakferret/bin/leakferret
|
|
43
|
+
- lib/leakferret/binary.rb
|
|
44
|
+
- lib/leakferret/client.rb
|
|
45
|
+
- lib/leakferret/error.rb
|
|
46
|
+
- lib/leakferret/platform.rb
|
|
47
|
+
- lib/leakferret/version.rb
|
|
48
|
+
homepage: https://github.com/leakferrethq/leakferret-ruby
|
|
49
|
+
licenses:
|
|
50
|
+
- MIT
|
|
51
|
+
metadata:
|
|
52
|
+
homepage_uri: https://github.com/leakferrethq/leakferret-ruby
|
|
53
|
+
source_code_uri: https://github.com/leakferrethq/leakferret-ruby
|
|
54
|
+
changelog_uri: https://github.com/leakferrethq/leakferret-ruby/blob/main/CHANGELOG.md
|
|
55
|
+
documentation_uri: https://rubydoc.info/gems/leakferret
|
|
56
|
+
rubygems_mfa_required: 'true'
|
|
57
|
+
post_install_message:
|
|
58
|
+
rdoc_options: []
|
|
59
|
+
require_paths:
|
|
60
|
+
- lib
|
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: 3.1.0
|
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0'
|
|
71
|
+
requirements: []
|
|
72
|
+
rubygems_version: 3.5.22
|
|
73
|
+
signing_key:
|
|
74
|
+
specification_version: 4
|
|
75
|
+
summary: Context-aware secret detection (Ruby wrapper for the leakferret binary).
|
|
76
|
+
test_files: []
|