lockfile_audit 0.1.0
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/CHANGELOG.md +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +132 -0
- data/Rakefile +10 -0
- data/lib/lockfile_audit/errors.rb +34 -0
- data/lib/lockfile_audit/package_collector.rb +65 -0
- data/lib/lockfile_audit/report.rb +40 -0
- data/lib/lockfile_audit/version.rb +5 -0
- data/lib/lockfile_audit/vulnerability_collector.rb +134 -0
- data/lib/lockfile_audit.rb +33 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6f02ad30c5d73027023967b15462cf39489383751cd89acb440fb0c6eb993e70
|
|
4
|
+
data.tar.gz: a4d6a9fbd40231af0c64935759faf225f5891bb93a617723cc540bd1adacd2c2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7c0fc2776fe8bc32ae760225ac8d76ed805dc8e839fb85fb27d60aae31894608a41b6f341eecdef0e396149a7bc4bd8da7cf136ea79a782e53e73b018c73bbb9
|
|
7
|
+
data.tar.gz: 6b62f3b0e60043aa76e4bc771adb14082df5f36f6a2afe34f621dbc299b7f1bdc59852666e855f0c1bf6548e1442cd3468b5065cbbc80a2558b6b5b96f2ca03b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-09-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `LockfileAudit.report` returns a JSON-ready Hash with `generated_at`,
|
|
15
|
+
`packages` (from `Gemfile.lock`), and `audit.vulnerabilities`
|
|
16
|
+
(from `bundler-audit`).
|
|
17
|
+
- `LockfileAudit::PackageCollector` parses every `specs:` block in a
|
|
18
|
+
`Gemfile.lock`, collecting top-level gems with their resolved versions.
|
|
19
|
+
- `LockfileAudit::VulnerabilityCollector` normalizes `bundle-audit
|
|
20
|
+
check --format json` output, tolerating both the Array and
|
|
21
|
+
Hash-with-`results` schema shapes.
|
|
22
|
+
- `LockfileAudit::Report` assembles the final payload and stamps
|
|
23
|
+
`generated_at` in ISO 8601 UTC.
|
|
24
|
+
- Support for passing pre-computed audit JSON via the `audit_json:`
|
|
25
|
+
keyword, so `bundler-audit` need not be installed or invoked.
|
|
26
|
+
- `LockfileAudit::Errors` namespace with `LockfileNotFound`,
|
|
27
|
+
`BundlerAuditNotInstalled`, and `InvalidAuditJson`.
|
|
28
|
+
|
|
29
|
+
[Unreleased]: https://github.com/Behnam1369/lockfile_audit/compare/v0.1.0...HEAD
|
|
30
|
+
[0.1.0]: https://github.com/Behnam1369/lockfile_audit/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 behnam1369
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# LockfileAudit
|
|
2
|
+
|
|
3
|
+
Returns a JSON package inventory plus vulnerability audit data for a Ruby
|
|
4
|
+
project, in a single payload.
|
|
5
|
+
|
|
6
|
+
`lockfile_audit` parses a `Gemfile.lock` for the resolved gem list and pairs it
|
|
7
|
+
with findings from [bundler-audit](https://github.com/rubysec/bundler-audit).
|
|
8
|
+
The result is a plain, JSON-ready Hash you can expose from a Rails endpoint,
|
|
9
|
+
feed into a dashboard, or archive for compliance.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add it to your Gemfile:
|
|
14
|
+
|
|
15
|
+
gem "lockfile_audit"
|
|
16
|
+
|
|
17
|
+
Then run `bundle install`.
|
|
18
|
+
|
|
19
|
+
To collect vulnerability data, `bundler-audit` must be available on your
|
|
20
|
+
PATH:
|
|
21
|
+
|
|
22
|
+
gem install bundler-audit
|
|
23
|
+
bundle-audit update # fetch the advisory database
|
|
24
|
+
|
|
25
|
+
If you would rather manage that step yourself, you can pass pre-computed
|
|
26
|
+
audit JSON instead — see Usage.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Basic
|
|
31
|
+
|
|
32
|
+
require "lockfile_audit"
|
|
33
|
+
|
|
34
|
+
LockfileAudit.report(gemfile_lock_path: "Gemfile.lock")
|
|
35
|
+
|
|
36
|
+
This returns:
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
"generated_at": "2026-09-26T11:09:28Z",
|
|
40
|
+
"packages": [
|
|
41
|
+
{ "name": "rails", "version": "7.1.3" },
|
|
42
|
+
{ "name": "nokogiri", "version": "1.16.2" },
|
|
43
|
+
{ "name": "pg", "version": "1.5.6" }
|
|
44
|
+
],
|
|
45
|
+
"audit": {
|
|
46
|
+
"vulnerabilities": [
|
|
47
|
+
{
|
|
48
|
+
"gem": "nokogiri",
|
|
49
|
+
"version": "1.16.2",
|
|
50
|
+
"criticality": "High",
|
|
51
|
+
"description": "Nokogiri Command Injection Vulnerability"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
### Passing pre-computed audit JSON
|
|
58
|
+
|
|
59
|
+
If you already run `bundle-audit` in your own CI pipeline, or want to avoid
|
|
60
|
+
shelling out at request time, pass its JSON output directly:
|
|
61
|
+
|
|
62
|
+
audit_json = File.read("tmp/bundle-audit.json")
|
|
63
|
+
|
|
64
|
+
LockfileAudit.report(
|
|
65
|
+
gemfile_lock_path: "Gemfile.lock",
|
|
66
|
+
audit_json: audit_json
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
When `audit_json:` is provided, `lockfile_audit` does not invoke
|
|
70
|
+
`bundler-audit` at all.
|
|
71
|
+
|
|
72
|
+
### From a Rails controller
|
|
73
|
+
|
|
74
|
+
class PackageAuditController < ApplicationController
|
|
75
|
+
def show
|
|
76
|
+
render json: LockfileAudit.report(
|
|
77
|
+
gemfile_lock_path: Rails.root.join("Gemfile.lock").to_s
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
## Payload schema
|
|
83
|
+
|
|
84
|
+
| Key | Type | Notes |
|
|
85
|
+
|-----|------|-------|
|
|
86
|
+
| `generated_at` | String | ISO 8601 UTC timestamp, e.g. `"2026-09-26T11:09:28Z"` |
|
|
87
|
+
| `packages` | Array | Sorted by `name`; each entry has `name` and `version` |
|
|
88
|
+
| `audit` | Hash | Contains a single `vulnerabilities` key |
|
|
89
|
+
| `audit.vulnerabilities` | Array | Each entry has `gem`, `version`, `criticality`, and `description` |
|
|
90
|
+
|
|
91
|
+
All keys are symbols when you consume the Hash in Ruby, and strings when
|
|
92
|
+
serialized to JSON. `description` falls back to the advisory title when no
|
|
93
|
+
longer description is available.
|
|
94
|
+
|
|
95
|
+
## Errors
|
|
96
|
+
|
|
97
|
+
All errors inherit from `LockfileAudit::Errors::Error`.
|
|
98
|
+
|
|
99
|
+
| Error | Raised when |
|
|
100
|
+
|-------|-------------|
|
|
101
|
+
| `LockfileNotFound` | The `Gemfile.lock` file does not exist at the given path |
|
|
102
|
+
| `BundlerAuditNotInstalled` | `bundle-audit` is not on `PATH` and no `audit_json:` was passed |
|
|
103
|
+
| `InvalidAuditJson` | The supplied audit JSON cannot be parsed |
|
|
104
|
+
|
|
105
|
+
## How it works
|
|
106
|
+
|
|
107
|
+
- Packages are read from the `specs:` blocks of `Gemfile.lock`. Only
|
|
108
|
+
top-level specs are collected; transitive dependency lines (which carry a
|
|
109
|
+
version constraint rather than a resolved version) are ignored.
|
|
110
|
+
- Vulnerabilities come from `bundle-audit check --format json`. The output
|
|
111
|
+
is normalized to tolerate both known schema shapes: a bare Array of
|
|
112
|
+
findings, or a Hash with a `results` key.
|
|
113
|
+
- The `bundle-audit` subprocess runs with a scrubbed environment, so the
|
|
114
|
+
parent process's Bundler state (`BUNDLE_*`, `RUBYOPT`) does not leak into
|
|
115
|
+
it. This makes the collector safe to call from inside a running Rails app.
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
bundle install
|
|
120
|
+
bundle exec rspec
|
|
121
|
+
bundle exec standardrb
|
|
122
|
+
|
|
123
|
+
To release a new version:
|
|
124
|
+
|
|
125
|
+
1. Bump `lib/lockfile_audit/version.rb`.
|
|
126
|
+
2. Update `CHANGELOG.md`.
|
|
127
|
+
3. `gem build lockfile_audit.gemspec`
|
|
128
|
+
4. `gem push lockfile_audit-<version>.gem`
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT. See LICENSE.txt.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LockfileAudit
|
|
4
|
+
# Namespace for all errors raised by LockfileAudit.
|
|
5
|
+
module Errors
|
|
6
|
+
# Base class for every LockfileAudit error.
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Raised when the Gemfile.lock file does not exist at the given path.
|
|
10
|
+
class LockfileNotFound < Error
|
|
11
|
+
def initialize(path)
|
|
12
|
+
super("Gemfile.lock not found at: #{path}")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Raised when the `bundle-audit` executable cannot be found on PATH.
|
|
17
|
+
class BundlerAuditNotInstalled < Error
|
|
18
|
+
def initialize(message = nil)
|
|
19
|
+
super(message || "bundler-audit is not installed or not on PATH")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Raised when `bundle-audit` runs but exits with a non-zero status
|
|
24
|
+
# other than 1 (which conventionally means "vulnerabilities found").
|
|
25
|
+
class BundlerAuditFailed < Error; end
|
|
26
|
+
|
|
27
|
+
# Raised when the supplied audit JSON cannot be parsed.
|
|
28
|
+
class InvalidAuditJson < Error
|
|
29
|
+
def initialize(message = nil)
|
|
30
|
+
super("Could not parse bundler-audit JSON: #{message}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LockfileAudit
|
|
4
|
+
# Parses a Gemfile.lock and returns the resolved gem list.
|
|
5
|
+
#
|
|
6
|
+
# Only top-level specs from `specs:` blocks are collected. Transitive
|
|
7
|
+
# dependency lines are skipped because they:
|
|
8
|
+
# - are indented further (6 spaces instead of 4), and
|
|
9
|
+
# - carry a version constraint (e.g. "= 7.1.3.2") rather than a
|
|
10
|
+
# resolved version.
|
|
11
|
+
#
|
|
12
|
+
# A Gemfile.lock may contain multiple specs blocks (e.g. PATH, GEM, GIT),
|
|
13
|
+
# and all are collected.
|
|
14
|
+
class PackageCollector
|
|
15
|
+
# Matches a top-level spec line such as:
|
|
16
|
+
# " rails (7.1.3)"
|
|
17
|
+
# Exactly four leading spaces, then the gem name, then a version in
|
|
18
|
+
# parentheses. The version itself must not start with a comparator
|
|
19
|
+
# operator (=, <, >, ~), which would indicate a transitive dependency.
|
|
20
|
+
SPEC_LINE = /\A {4}([^\s(]+) \(([^)=<>~!]+)\)\z/
|
|
21
|
+
|
|
22
|
+
# Matches a "specs:" header line at any indentation depth.
|
|
23
|
+
SPECS_HEADER = /\A\s*specs:\s*\z/
|
|
24
|
+
|
|
25
|
+
# @param gemfile_lock_path [String, Pathname] path to the Gemfile.lock file
|
|
26
|
+
def initialize(gemfile_lock_path:)
|
|
27
|
+
@gemfile_lock_path = gemfile_lock_path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Array<Hash>] array of `{ name:, version: }` sorted by name
|
|
31
|
+
# @raise [Errors::LockfileNotFound] if the lockfile does not exist
|
|
32
|
+
def call
|
|
33
|
+
unless File.exist?(@gemfile_lock_path)
|
|
34
|
+
raise Errors::LockfileNotFound, @gemfile_lock_path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
in_specs = false
|
|
38
|
+
packages = []
|
|
39
|
+
|
|
40
|
+
File.foreach(@gemfile_lock_path) do |raw_line|
|
|
41
|
+
line = raw_line.chomp
|
|
42
|
+
|
|
43
|
+
if line.match?(SPECS_HEADER)
|
|
44
|
+
in_specs = true
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A non-indented, non-empty line (e.g. "PLATFORMS" or "GEM")
|
|
49
|
+
# ends the current specs block.
|
|
50
|
+
in_specs = false if in_specs && !line.start_with?(" ")
|
|
51
|
+
|
|
52
|
+
next unless in_specs
|
|
53
|
+
|
|
54
|
+
match = line.match(SPEC_LINE)
|
|
55
|
+
next unless match
|
|
56
|
+
|
|
57
|
+
packages << {name: match[1], version: match[2]}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
packages
|
|
61
|
+
.uniq { |pkg| pkg[:name] }
|
|
62
|
+
.sort_by { |pkg| pkg[:name] }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module LockfileAudit
|
|
6
|
+
# Assembles the final payload by combining packages and vulnerabilities.
|
|
7
|
+
class Report
|
|
8
|
+
# @param gemfile_lock_path [String, Pathname] path to the Gemfile.lock file
|
|
9
|
+
# @param audit_json [String, nil] pre-computed bundler-audit JSON; if nil,
|
|
10
|
+
# bundler-audit is invoked directly
|
|
11
|
+
def initialize(gemfile_lock_path: "Gemfile.lock", audit_json: nil)
|
|
12
|
+
@gemfile_lock_path = gemfile_lock_path
|
|
13
|
+
@audit_json = audit_json
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Hash] payload with `:generated_at`, `:packages`, and `:audit`
|
|
17
|
+
def call
|
|
18
|
+
{
|
|
19
|
+
generated_at: Time.now.utc.iso8601,
|
|
20
|
+
packages: packages,
|
|
21
|
+
audit: {
|
|
22
|
+
vulnerabilities: vulnerabilities
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def packages
|
|
30
|
+
PackageCollector.new(gemfile_lock_path: @gemfile_lock_path).call
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def vulnerabilities
|
|
34
|
+
VulnerabilityCollector.new(
|
|
35
|
+
audit_json: @audit_json,
|
|
36
|
+
lockfile_path: @gemfile_lock_path
|
|
37
|
+
).call
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module LockfileAudit
|
|
7
|
+
# Collects vulnerability findings from bundler-audit.
|
|
8
|
+
#
|
|
9
|
+
# Either accepts pre-computed JSON (via +audit_json:+) or shells out to
|
|
10
|
+
# `bundle-audit check --format json` when no JSON is supplied.
|
|
11
|
+
#
|
|
12
|
+
# The normalizer tolerates both known shapes of bundler-audit output:
|
|
13
|
+
# - a bare Array of findings (newer versions)
|
|
14
|
+
# - a Hash with a "results" key holding the Array (older versions)
|
|
15
|
+
#
|
|
16
|
+
# When invoking the CLI, the collector runs with a scrubbed environment so
|
|
17
|
+
# the parent process's Bundler state (BUNDLE_*, RUBYOPT, etc.) does not leak
|
|
18
|
+
# into the subprocess. Without this, running from inside a Rails console
|
|
19
|
+
# causes Bundler to re-validate the Ruby version in the subprocess and abort
|
|
20
|
+
# with Bundler::RubyVersionMismatch.
|
|
21
|
+
#
|
|
22
|
+
# The CLI is invoked directly (never via `bundle exec` or `gem exec`) so the
|
|
23
|
+
# subprocess stays independent of the parent's Bundler context and does not
|
|
24
|
+
# trigger dependency installation or RDoc generation as a side effect.
|
|
25
|
+
class VulnerabilityCollector
|
|
26
|
+
# Environment variables that must not leak into the bundler-audit
|
|
27
|
+
# subprocess. These are set by Bundler when the parent process is a
|
|
28
|
+
# bundled Ruby app, and their presence forces Bundler to re-initialize
|
|
29
|
+
# inside the subprocess.
|
|
30
|
+
BUNDLER_ENV_KEYS = %w[
|
|
31
|
+
BUNDLE_GEMFILE
|
|
32
|
+
BUNDLE_BIN_PATH
|
|
33
|
+
BUNDLE_PATH
|
|
34
|
+
BUNDLE_APP_CONFIG
|
|
35
|
+
BUNDLE_FROZEN
|
|
36
|
+
BUNDLE_WITHOUT
|
|
37
|
+
BUNDLE_WITH
|
|
38
|
+
BUNDLE_JOBS
|
|
39
|
+
BUNDLE_RETRY
|
|
40
|
+
BUNDLER_VERSION
|
|
41
|
+
BUNDLER_ORIG_BUNDLE_BIN_PATH
|
|
42
|
+
BUNDLER_ORIG_BUNDLE_GEMFILE
|
|
43
|
+
BUNDLER_ORIG_BUNDLE_PATH
|
|
44
|
+
BUNDLER_ORIG_MANPATH
|
|
45
|
+
BUNDLER_ORIG_RUBYLIB
|
|
46
|
+
BUNDLER_ORIG_RUBYOPT
|
|
47
|
+
RUBYOPT
|
|
48
|
+
RUBYLIB
|
|
49
|
+
].freeze
|
|
50
|
+
|
|
51
|
+
# @param audit_json [String, nil] raw JSON from bundler-audit; if nil,
|
|
52
|
+
# bundler-audit is invoked directly
|
|
53
|
+
# @param lockfile_path [String, Pathname] path to Gemfile.lock; used to
|
|
54
|
+
# derive the working directory for the CLI invocation
|
|
55
|
+
def initialize(audit_json: nil, lockfile_path: "Gemfile.lock")
|
|
56
|
+
@audit_json = audit_json
|
|
57
|
+
@lockfile_path = lockfile_path
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Array<Hash>] normalized findings, each with keys
|
|
61
|
+
# `:gem`, `:version`, `:criticality`, `:description`
|
|
62
|
+
# @raise [Errors::InvalidAuditJson] if the JSON cannot be parsed
|
|
63
|
+
# @raise [Errors::BundlerAuditNotInstalled] if bundler-audit is missing
|
|
64
|
+
def call
|
|
65
|
+
raw = @audit_json || run_bundler_audit
|
|
66
|
+
return [] if raw.nil? || raw.strip.empty?
|
|
67
|
+
|
|
68
|
+
parsed = JSON.parse(raw)
|
|
69
|
+
results = parsed.is_a?(Hash) ? parsed.fetch("results", []) : parsed
|
|
70
|
+
|
|
71
|
+
Array(results).map { |finding| normalize(finding) }
|
|
72
|
+
rescue JSON::ParserError => e
|
|
73
|
+
raise Errors::InvalidAuditJson, e.message
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def run_bundler_audit
|
|
79
|
+
stdout, stderr, status = invoke(["bundle-audit"])
|
|
80
|
+
return stdout if usable?(stdout, status)
|
|
81
|
+
|
|
82
|
+
raise Errors::BundlerAuditNotInstalled,
|
|
83
|
+
"bundle-audit could not be invoked. " \
|
|
84
|
+
"Ensure the `bundler-audit` gem is installed and on your PATH " \
|
|
85
|
+
"(e.g. `gem install bundler-audit`). " \
|
|
86
|
+
"stderr: #{stderr.to_s.strip}"
|
|
87
|
+
rescue Errno::ENOENT
|
|
88
|
+
raise Errors::BundlerAuditNotInstalled,
|
|
89
|
+
"bundle-audit is not installed or not on PATH. " \
|
|
90
|
+
"Install it with `gem install bundler-audit`."
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def invoke(command)
|
|
94
|
+
Open3.capture3(
|
|
95
|
+
clean_env,
|
|
96
|
+
*command, "check", "--format", "json",
|
|
97
|
+
chdir: File.dirname(File.expand_path(@lockfile_path))
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Builds an environment hash that removes the inherited Bundler state.
|
|
102
|
+
# Passing nil as the value to Open3 unsets the variable in the child.
|
|
103
|
+
def clean_env
|
|
104
|
+
BUNDLER_ENV_KEYS.each_with_object({}) do |key, acc|
|
|
105
|
+
acc[key] = nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# bundler-audit exits 1 when it finds vulnerabilities (that's success for
|
|
110
|
+
# us), and 0 when the lockfile is clean. Any other exit code, or empty
|
|
111
|
+
# stdout, means the invocation didn't produce usable output.
|
|
112
|
+
def usable?(stdout, status)
|
|
113
|
+
return false if stdout.nil? || stdout.strip.empty?
|
|
114
|
+
return true if [0, 1].include?(status.exitstatus)
|
|
115
|
+
|
|
116
|
+
false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Maps a single bundler-audit finding onto the public schema.
|
|
120
|
+
#
|
|
121
|
+
# bundler-audit has used slightly different key names across versions, so
|
|
122
|
+
# we fall back to nested "advisory" keys when the top-level ones are absent.
|
|
123
|
+
def normalize(finding)
|
|
124
|
+
advisory = finding["advisory"] || {}
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
gem: finding["gem"] || advisory["package"],
|
|
128
|
+
version: finding["version"] || advisory["affected_versions"],
|
|
129
|
+
criticality: finding["criticality"] || advisory["criticality"],
|
|
130
|
+
description: finding["title"] || advisory["title"] || finding["description"]
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lockfile_audit/version"
|
|
4
|
+
require_relative "lockfile_audit/errors"
|
|
5
|
+
require_relative "lockfile_audit/package_collector"
|
|
6
|
+
require_relative "lockfile_audit/vulnerability_collector"
|
|
7
|
+
require_relative "lockfile_audit/report"
|
|
8
|
+
|
|
9
|
+
# LockfileAudit parses a Gemfile.lock for resolved gems and reports
|
|
10
|
+
# vulnerabilities from bundler-audit as a single JSON-ready hash.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# LockfileAudit.report(gemfile_lock_path: "Gemfile.lock")
|
|
14
|
+
# # => {
|
|
15
|
+
# # generated_at: "2026-09-26T12:00:00Z",
|
|
16
|
+
# # packages: [{ name: "rails", version: "7.1.3" }, ...],
|
|
17
|
+
# # audit: { vulnerabilities: [{ gem: "nokogiri", ... }] }
|
|
18
|
+
# # }
|
|
19
|
+
module LockfileAudit
|
|
20
|
+
# Builds the full report payload.
|
|
21
|
+
#
|
|
22
|
+
# @param gemfile_lock_path [String, Pathname] path to the Gemfile.lock file
|
|
23
|
+
# @param audit_json [String, nil] pre-computed JSON from
|
|
24
|
+
# `bundle-audit check --format json`. When nil, bundler-audit is invoked
|
|
25
|
+
# directly.
|
|
26
|
+
# @return [Hash] a JSON-ready hash matching the documented payload shape
|
|
27
|
+
def self.report(gemfile_lock_path: "Gemfile.lock", audit_json: nil)
|
|
28
|
+
Report.new(
|
|
29
|
+
gemfile_lock_path: gemfile_lock_path,
|
|
30
|
+
audit_json: audit_json
|
|
31
|
+
).call
|
|
32
|
+
end
|
|
33
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lockfile_audit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- behnam1369
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Parses Gemfile.lock for resolved gems and reports vulnerabilities from
|
|
13
|
+
bundler-audit as a single JSON-ready hash.
|
|
14
|
+
email:
|
|
15
|
+
- behnam.aghaali@yahoo.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- LICENSE.txt
|
|
22
|
+
- README.md
|
|
23
|
+
- Rakefile
|
|
24
|
+
- lib/lockfile_audit.rb
|
|
25
|
+
- lib/lockfile_audit/errors.rb
|
|
26
|
+
- lib/lockfile_audit/package_collector.rb
|
|
27
|
+
- lib/lockfile_audit/report.rb
|
|
28
|
+
- lib/lockfile_audit/version.rb
|
|
29
|
+
- lib/lockfile_audit/vulnerability_collector.rb
|
|
30
|
+
homepage: https://github.com/Behnam1369/lockfile_audit
|
|
31
|
+
licenses:
|
|
32
|
+
- MIT
|
|
33
|
+
metadata:
|
|
34
|
+
homepage_uri: https://github.com/Behnam1369/lockfile_audit
|
|
35
|
+
changelog_uri: https://github.com/Behnam1369/lockfile_audit/blob/main/CHANGELOG.md
|
|
36
|
+
rdoc_options: []
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: 3.2.0
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubygems_version: 3.6.9
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: JSON package inventory plus vulnerability audit from a Gemfile.lock.
|
|
53
|
+
test_files: []
|