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 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
@@ -0,0 +1,8 @@
1
+ --markup markdown
2
+ --readme README.md
3
+ --title "leakferret (Ruby)"
4
+ --no-private
5
+ --charset utf-8
6
+ lib/**/*.rb
7
+ -
8
+ LICENSE.txt
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
+ [![Gem Version](https://img.shields.io/gem/v/leakferret?logo=rubygems&logoColor=white)](https://rubygems.org/gems/leakferret)
10
+ [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/leakferrethq/leakferret/blob/master/LICENSE.txt)
11
+ [![MCP Badge](https://lobehub.com/badge/mcp/leakferrethq-leakferret-ruby)](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 &lt;missusk@protonmail.com&gt;.
data/exe/leakferret ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Pass-through to the bundled native binary. Same CLI as the Rust crate.
5
+ require 'leakferret'
6
+
7
+ bin = Leakferret::Binary.path
8
+ exec(bin, *ARGV)
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: []