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 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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LockfileAudit
4
+ VERSION = "0.1.0"
5
+ 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: []