leakferret 0.1.3

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: 77f46588fbab40b45095c4654047ef78ab41203036e77e3df34e6c0af9396777
4
+ data.tar.gz: '079c0db268161032b79eac1e49fee61f785aa334a242680f95d7c8a7d260d8f7'
5
+ SHA512:
6
+ metadata.gz: 89f676f8d255335e83d820a5c962a723cc6434039b56180e2e0bdddcb017bc2d03d765139a00fe5827e6c1b82125a351617bd0cec8a225d9332e76f18231c339
7
+ data.tar.gz: 151b37249dbf4a8d743bc142d8dfb0815a16e5a7833be451dfa0640316026d21fa36e5215785552e683693594f4ed1bc614145401235edccb29fcc8a4aa62736
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,123 @@
1
+ <p align="center">
2
+ <img src="assets/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
+ Ruby gem wrapper around the native [`leakferret`](https://github.com/leakferrethq/leakferret)
10
+ binary. This gem ships no scanning logic of its own: it installs a tiny Ruby
11
+ shim plus a small executable, and downloads the prebuilt, statically-linked
12
+ binary (written in Rust) from GitHub Releases once per platform at install
13
+ time. All the work — scan, classify, verify, rewrite — happens in that single
14
+ binary.
15
+
16
+ This is the same packaging pattern used by `ruff`, `biome`, and `esbuild`:
17
+ distributing the toolchain to build a Rust engine on every machine is
18
+ unfriendly, so we ship the compiled engine instead.
19
+
20
+ ## What leakferret does
21
+
22
+ leakferret finds hardcoded secrets and API keys in your code and helps you
23
+ remove them, in five stations:
24
+
25
+ 1. **Scan** — regex pre-filter over files; respects `.gitignore` and also reads
26
+ dotfiles like `.env`.
27
+ 2. **Catalog** — a signed database of known-public example credentials (Stripe
28
+ test keys, `AKIAIOSFODNN7EXAMPLE`, jwt.io samples) so documented examples are
29
+ marked `FIXTURE` instead of false-alarming.
30
+ 3. **Classify** — a `REAL` / `FIXTURE` / `UNKNOWN` verdict, from offline
31
+ heuristics or by asking the host editor/agent language model (no extra API
32
+ key, no cost).
33
+ 4. **Verify** — a real but harmless API call to the provider (AWS SigV4,
34
+ GitHub, GitLab, Stripe, OpenAI, Anthropic, Slack, Twilio, SendGrid, Mailgun,
35
+ Datadog, Heroku, npm, PyPI, DigitalOcean) to confirm a key is live, plus a
36
+ trufflehog fallback.
37
+ 5. **Rewrite** — swap a hardcoded literal for an environment-variable lookup
38
+ (`ENV.fetch` in Ruby, `process.env` in JS, `os.environ` in Python), add a
39
+ `.env.example` line, and print secret-manager seed commands.
40
+
41
+ **Privacy invariant:** the full secret value never leaves your machine. Only a
42
+ redacted first-4-plus-last-4 preview (e.g. `AKIA...4XYZ`) is ever written to a
43
+ report, log, network message, or model prompt. Verification calls go straight
44
+ from your machine to the provider — leakferret has no servers.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ gem install leakferret
50
+ ```
51
+
52
+ This downloads `leakferret-{version}-{platform}.tar.gz` from GitHub Releases and
53
+ unpacks the binary into `lib/leakferret/bin/`.
54
+
55
+ Add it to a `Gemfile` for project-local use:
56
+
57
+ ```ruby
58
+ gem 'leakferret'
59
+ ```
60
+
61
+ Requires Ruby >= 3.1.
62
+
63
+ ## CLI
64
+
65
+ The gem installs a `leakferret` executable that simply `exec`s the binary, so
66
+ every subcommand and flag works exactly as upstream:
67
+
68
+ ```bash
69
+ leakferret scan .
70
+ leakferret verify . --only-verified
71
+ leakferret rewrite . --apply --backend doppler
72
+ leakferret baseline init
73
+ leakferret catalog info
74
+ leakferret mcp # MCP server on stdio
75
+ ```
76
+
77
+ `leakferret scan --git` walks commit history. Output formats are `pretty`
78
+ (colored terminal), `json`, and `sarif` (for GitHub Code Scanning).
79
+
80
+ ## Ruby API
81
+
82
+ ```ruby
83
+ require 'leakferret'
84
+
85
+ # Regex pre-filter only.
86
+ findings = Leakferret.scan('.')
87
+
88
+ # + provider-verified (live HTTP to GitHub / Stripe / AWS / ...).
89
+ findings = Leakferret.verify('.', mode: 'only-verified')
90
+
91
+ # + propose rewrites for REAL findings.
92
+ findings = Leakferret.rewrite('.', backend: 'doppler')
93
+
94
+ # Apply rewrites in place.
95
+ Leakferret.rewrite('.', apply: true)
96
+ ```
97
+
98
+ Each `Finding` is a hash with `path`, `line`, `column`, `pattern`, `severity`,
99
+ `verdict`, `match_redacted`, `confidence`, `verification`, and `fingerprint`.
100
+
101
+ ## Using a local binary
102
+
103
+ Every leakferret wrapper honors the `LEAKFERRET_BIN` environment variable. Point
104
+ it at a binary on disk and the wrapper runs that instead of the downloaded copy:
105
+
106
+ ```bash
107
+ export LEAKFERRET_BIN=/opt/leakferret/leakferret
108
+ leakferret scan .
109
+ ```
110
+
111
+ For air-gapped or offline installs, set `LEAKFERRET_SKIP_DOWNLOAD=1` to skip the
112
+ release download and position the binary yourself.
113
+
114
+ ## License
115
+
116
+ MIT for this gem and the bundled binary. The fixture catalog **data** is
117
+ CC-BY-SA-4.0 — see [`leakferret-catalog`](https://github.com/leakferrethq/leakferret-catalog).
118
+
119
+ ---
120
+
121
+ Part of [leakferret](https://github.com/leakferrethq/leakferret) ·
122
+ [leakferret.com](https://leakferret.com) ·
123
+ 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)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Best-effort install-time pre-fetch of the native binary into the
4
+ # user-writable cache (see lib/leakferret/binary.rb). This is purely an
5
+ # optimisation so the first invocation is instant — the binary is also
6
+ # downloaded lazily on first use, so a failure here is harmless. Set
7
+ # LEAKFERRET_SKIP_DOWNLOAD to skip the pre-fetch.
8
+ #
9
+ # We deliberately do NOT compile the Rust source: that would force every
10
+ # user to have a Rust toolchain and wait for a long build. The
11
+ # download-binary model matches ruff (Python), biome/esbuild (npm).
12
+
13
+ require_relative '../../lib/leakferret/version'
14
+ require_relative '../../lib/leakferret/platform'
15
+ require_relative '../../lib/leakferret/error'
16
+ require_relative '../../lib/leakferret/binary'
17
+
18
+ # Emit an empty Makefile so rubygems considers the "extension" built.
19
+ File.write('Makefile', "all:\n\t@true\ninstall:\n\t@true\nclean:\n\t@true\n")
20
+
21
+ def log(msg)
22
+ warn "[leakferret/extconf] #{msg}"
23
+ end
24
+
25
+ if ENV['LEAKFERRET_SKIP_DOWNLOAD']
26
+ log 'LEAKFERRET_SKIP_DOWNLOAD set; binary will be downloaded on first use.'
27
+ exit 0
28
+ end
29
+
30
+ begin
31
+ path = Leakferret::Binary.ensure!
32
+ log "binary ready at #{path}"
33
+ rescue StandardError => e
34
+ log "pre-fetch failed (#{e.class}: #{e.message}); it will download on first use."
35
+ end
36
+
37
+ # Always succeed: a failed pre-fetch must not fail the gem install.
38
+ exit 0
@@ -0,0 +1,120 @@
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 (and, if needed, downloads) the native `leakferret` binary.
11
+ #
12
+ # The binary is fetched into an absolute, user-writable cache directory
13
+ # rather than into the gem's own tree. RubyGems builds extensions in a
14
+ # throwaway temp dir, so anything written relative to the gem during
15
+ # `gem install` is discarded — the cache path sidesteps that entirely and
16
+ # also lets a plain `gem install` (no extension) work.
17
+ module Binary
18
+ # A binary vendored inside the gem, if one was shipped (normally empty).
19
+ BUNDLED_DIR = Pathname.new(__dir__).join('bin').freeze
20
+
21
+ module_function
22
+
23
+ # Absolute path to the native binary, downloading it on first use if
24
+ # necessary. Resolution order:
25
+ # 1. LEAKFERRET_BIN — explicit override
26
+ # 2. lib/leakferret/bin/ — a binary vendored in the gem
27
+ # 3. the per-version cache — fetched on a prior run or at install
28
+ # 4. download into the cache now
29
+ def path
30
+ override = ENV['LEAKFERRET_BIN']
31
+ unless override.nil? || override.empty?
32
+ unless File.file?(override)
33
+ raise BinaryNotFoundError, "LEAKFERRET_BIN points to a missing file: #{override}"
34
+ end
35
+
36
+ return override
37
+ end
38
+
39
+ bundled = BUNDLED_DIR.join(Platform.binary_name)
40
+ return bundled.to_s if bundled.file?
41
+
42
+ return cache_path.to_s if cache_path.file?
43
+
44
+ ensure!
45
+ raise BinaryNotFoundError, install_instructions(cache_path) unless cache_path.file?
46
+
47
+ cache_path.to_s
48
+ end
49
+
50
+ # User-writable cache directory, namespaced by the binary version so a
51
+ # gem upgrade fetches a fresh binary instead of reusing a stale one.
52
+ def cache_dir
53
+ base =
54
+ if Platform.windows?
55
+ ENV['LOCALAPPDATA'] || File.join(Dir.home, 'AppData', 'Local')
56
+ else
57
+ ENV['XDG_CACHE_HOME'] || File.join(Dir.home, '.cache')
58
+ end
59
+ Pathname.new(base).join('leakferret', BINARY_VERSION)
60
+ end
61
+
62
+ def cache_path
63
+ cache_dir.join(Platform.binary_name)
64
+ end
65
+
66
+ def download_url
67
+ 'https://github.com/leakferrethq/leakferret/releases/download/' \
68
+ "v#{BINARY_VERSION}/leakferret-#{BINARY_VERSION}-#{Platform.triple}.tar.gz"
69
+ end
70
+
71
+ # Download and unpack the binary into the cache. Idempotent: a no-op when
72
+ # the binary is already cached. Returns the path; raises on failure.
73
+ def ensure!
74
+ dest = cache_path
75
+ return dest.to_s if dest.file?
76
+
77
+ require 'fileutils'
78
+ require 'open-uri'
79
+ require 'zlib'
80
+ require 'rubygems/package'
81
+
82
+ FileUtils.mkdir_p(dest.dirname)
83
+ # Stream download -> gunzip -> untar in pure Ruby (no external `tar`,
84
+ # which on Windows mis-reads `C:\` as a remote host). The archive nests
85
+ # everything under leakferret-<version>-<triple>/, so match by basename.
86
+ found = false
87
+ URI.open(download_url) do |io| # rubocop:disable Security/Open
88
+ Zlib::GzipReader.wrap(io) do |gz|
89
+ Gem::Package::TarReader.new(gz) do |tar|
90
+ tar.each do |entry|
91
+ next unless entry.file?
92
+ next unless File.basename(entry.full_name) == Platform.binary_name
93
+
94
+ File.binwrite(dest, entry.read)
95
+ found = true
96
+ end
97
+ end
98
+ end
99
+ end
100
+ raise BinaryNotFoundError, "binary not found inside #{download_url}" unless found
101
+
102
+ FileUtils.chmod(0o755, dest) unless Platform.windows?
103
+ dest.to_s
104
+ end
105
+
106
+ def install_instructions(candidate)
107
+ <<~MSG
108
+ leakferret native binary not found, and the automatic download failed.
109
+
110
+ Expected it at:
111
+ #{candidate}
112
+
113
+ Download the binary for your platform from:
114
+ https://github.com/leakferrethq/leakferret/releases
115
+
116
+ then either place it at the path above or point LEAKFERRET_BIN at it.
117
+ MSG
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+
6
+ module Leakferret
7
+ # Thin shell-out wrapper. Each public method invokes the binary with
8
+ # `--format json` and parses the resulting array.
9
+ class Client
10
+ def scan(path, exclude: [], only: nil, show_fixtures: false)
11
+ run(['scan', path, '--format', 'json'] + format_flags(exclude:, only:, show_fixtures:))
12
+ end
13
+
14
+ def verify(path, mode: 'best-effort', timeout: 10, **opts)
15
+ run(['verify', path, '--format', 'json', '--verify-mode', mode,
16
+ '--verifier-timeout-secs', timeout.to_s] + format_flags(**opts))
17
+ end
18
+
19
+ def rewrite(path, apply: false, backend: 'env', **opts)
20
+ args = ['rewrite', path, '--format', 'json', '--backend', backend]
21
+ args << '--apply' if apply
22
+ run(args + format_flags(**opts))
23
+ end
24
+
25
+ private
26
+
27
+ def format_flags(exclude: [], only: nil, show_fixtures: false)
28
+ flags = []
29
+ Array(exclude).each { |g| flags.push('--exclude', g) }
30
+ Array(only).each { |p| flags.push('--only', p) }
31
+ flags << '--show-fixtures' if show_fixtures
32
+ flags
33
+ end
34
+
35
+ def run(args)
36
+ out, err, status = Open3.capture3(Binary.path, *args)
37
+ unless [0, 1].include?(status.exitstatus)
38
+ raise BinaryInvocationError.new(
39
+ "leakferret exited with #{status.exitstatus}",
40
+ exit_status: status.exitstatus,
41
+ stderr: err,
42
+ )
43
+ end
44
+ JSON.parse(out.strip.empty? ? '[]' : out)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leakferret
4
+ class Error < StandardError; end
5
+ class BinaryNotFoundError < Error; end
6
+ class BinaryInvocationError < Error
7
+ attr_reader :exit_status, :stderr
8
+
9
+ def initialize(message, exit_status:, stderr:)
10
+ super(message)
11
+ @exit_status = exit_status
12
+ @stderr = stderr
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbconfig'
4
+
5
+ require_relative 'error'
6
+
7
+ module Leakferret
8
+ module Platform
9
+ module_function
10
+
11
+ def triple
12
+ cpu = case RbConfig::CONFIG['host_cpu']
13
+ when /x86_64|amd64|x64/ then 'x86_64'
14
+ when /aarch64|arm64/ then 'aarch64'
15
+ else
16
+ raise Error, "unsupported CPU: #{RbConfig::CONFIG['host_cpu']}"
17
+ end
18
+
19
+ case RbConfig::CONFIG['host_os']
20
+ when /mswin|mingw|cygwin/ then "#{cpu}-pc-windows-msvc"
21
+ when /darwin/ then "#{cpu}-apple-darwin"
22
+ when /linux/
23
+ # No aarch64-linux release asset yet (v0.1.0 ships x86_64 only).
24
+ raise Error, 'aarch64-linux has no prebuilt binary yet; build from source' if cpu == 'aarch64'
25
+
26
+ "#{cpu}-unknown-linux-gnu"
27
+ else
28
+ raise Error, "unsupported OS: #{RbConfig::CONFIG['host_os']}"
29
+ end
30
+ end
31
+
32
+ def binary_name
33
+ windows? ? 'leakferret.exe' : 'leakferret'
34
+ end
35
+
36
+ def windows?
37
+ RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leakferret
4
+ # The gem's own version.
5
+ VERSION = '0.1.3'
6
+
7
+ # The native binary release this gem downloads. Tracks the leakferret
8
+ # core release, which may move independently of the gem's own version
9
+ # (e.g. a gem-only bugfix).
10
+ BINARY_VERSION = '0.1.1'
11
+ end
data/lib/leakferret.rb ADDED
@@ -0,0 +1,48 @@
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` binary.
13
+ #
14
+ # The binary is downloaded once per platform at gem install time
15
+ # (`ext/leakferret/extconf.rb`) into `lib/leakferret/bin/`. Subsequent
16
+ # calls shell out to it and parse the JSON output.
17
+ module Leakferret
18
+ class << self
19
+ # Scan a directory; returns an array of finding hashes.
20
+ def scan(path = '.', **opts)
21
+ Client.new.scan(path, **opts)
22
+ end
23
+
24
+ # Scan + verify + classify; returns findings with verification +
25
+ # verdict filled in.
26
+ def verify(path = '.', **opts)
27
+ Client.new.verify(path, **opts)
28
+ end
29
+
30
+ # Scan + classify + propose rewrites for REAL findings. Use
31
+ # apply: true to write the rewrites in place.
32
+ def rewrite(path = '.', apply: false, **opts)
33
+ Client.new.rewrite(path, apply: apply, **opts)
34
+ end
35
+
36
+ # Path to the bundled binary. Useful for tooling integration.
37
+ def binary_path
38
+ Binary.path
39
+ end
40
+
41
+ # Version reported by the bundled binary (Rust) — may differ from
42
+ # the gem version during pre-release.
43
+ def binary_version
44
+ out, _err, _status = Open3.capture3(binary_path, '--version')
45
+ out.strip
46
+ end
47
+ end
48
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: leakferret
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Maria Khan
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ Context-aware secret scanning for Ruby projects. A thin wrapper around the
14
+ native leakferret binary (written in Rust): it finds hardcoded secrets,
15
+ confirms which ones are actually live by calling the provider, and rewrites
16
+ them to read from environment variables instead. The platform binary is
17
+ downloaded automatically on first use, so no Rust toolchain is required.
18
+
19
+ The API exposes Leakferret.scan, Leakferret.verify, and Leakferret.rewrite
20
+ (each returning Finding objects), plus a `leakferret` command-line tool.
21
+ email:
22
+ - missusk@protonmail.com
23
+ executables:
24
+ - leakferret
25
+ extensions:
26
+ - ext/leakferret/extconf.rb
27
+ extra_rdoc_files: []
28
+ files:
29
+ - LICENSE.txt
30
+ - README.md
31
+ - exe/leakferret
32
+ - ext/leakferret/extconf.rb
33
+ - lib/leakferret.rb
34
+ - lib/leakferret/binary.rb
35
+ - lib/leakferret/client.rb
36
+ - lib/leakferret/error.rb
37
+ - lib/leakferret/platform.rb
38
+ - lib/leakferret/version.rb
39
+ homepage: https://github.com/leakferrethq/leakferret-ruby
40
+ licenses:
41
+ - MIT
42
+ metadata:
43
+ homepage_uri: https://github.com/leakferrethq/leakferret-ruby
44
+ source_code_uri: https://github.com/leakferrethq/leakferret-ruby
45
+ changelog_uri: https://github.com/leakferrethq/leakferret-ruby/blob/main/CHANGELOG.md
46
+ rubygems_mfa_required: 'true'
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 3.1.0
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.6.9
62
+ specification_version: 4
63
+ summary: Context-aware secret detection (Ruby wrapper for the leakferret binary).
64
+ test_files: []