bootprint 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d4c97b0a32461ce90a144849e6fb9c1416682f0b682039cd5e89cfb2d2ca171
4
- data.tar.gz: d228014f4dca8d75d8aa03eab9b343e8ee0a9449e5cc8e50220c91549a305803
3
+ metadata.gz: 6ae8e01ef715b2bb8a62bedb57882a0cf2af5a783ff72c15673b46ecabbff4ea
4
+ data.tar.gz: a5c4b0fbf1d54ff4c65b0e338d1e117f6eaaf2d1f5c2f1a558824441af5cdca8
5
5
  SHA512:
6
- metadata.gz: ae4e15f6cd2655c988c3f7b845d417240e433b8e1384c5145b984bc0c243fbb0ad10c5fd09fb93cd8c38570b0682538a459e98db931a77c43c16686bf2963d71
7
- data.tar.gz: 7070a15b2d7a82018cb526c9b0fad201ea8259f87a6daa63641ad5be745fe9eee899348c33f41d1f80879e2a103cb6f3a89444eb65b839ec70660e587b76cc5c
6
+ metadata.gz: d536be2a2d5cd4059b6572736b555b2c2d4ddeaea2245f229dcca4d3d1e1d1726d7e35f4cc04a2b012c19212c8f32b31bb6fccbcc0a249ad152cccd475078123
7
+ data.tar.gz: 9d973eb86bcb2c82451af1799e80426fe17174b7b269d7bc75fb228e10027cd1d80f76866724935491a573f40d2d2cfeeac121e80f93d69fc06853ca4beb699c
data/ARCHITECTURE.md CHANGED
@@ -30,6 +30,8 @@ human / JSON / SARIF / Markdown / CI annotations
30
30
  - Formatters contain no detection logic.
31
31
  - Docker and security-audit code are lazy-loaded by their CLI commands.
32
32
  - Rails hooks load only when `Rails::Railtie` already exists; expensive profiling requires `BOOTPRINT_PROFILE_BOOT=1`.
33
+ - The VS Code extension is a workspace-trusted process adapter. It spawns the same Ruby CLI without a shell and contains no duplicate diagnosis logic.
34
+ - The public website is a dependency-free static surface under `site/`; it has no access to snapshots or application data and is deployed independently through GitHub Pages.
33
35
 
34
36
  ## Plugin contract
35
37
 
data/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to Bootprint are documented here. The project follows Semantic Versioning before and after 1.0 where practical.
4
4
 
5
+ ## Unreleased
6
+
7
+ ## 0.4.0 - 2026-08-11
8
+
9
+ ### Added
10
+
11
+ - `Bootprint::Advisories` for matching snapshot gem versions against offline JSON advisory bundles.
12
+ - `Bootprint.advisories` and `Bootprint.advise` convenience APIs.
13
+ - `bootprint advisories SNAPSHOT` CLI command with `--bundle` and `--format human|json` options.
14
+ - Sample empty advisory bundle schema at `data/advisories/schema/empty.json`.
15
+
16
+ ## 0.3.0 - 2026-08-04
17
+
18
+ ### Added
19
+
20
+ - `Bootprint::Matrix` for comparing two or more named snapshots at once.
21
+ - Majority-based consensus values for multi-environment drift analysis.
22
+ - Deterministic outlier identification and per-environment outlier counts.
23
+ - Missing-value reporting for incomplete staging, CI, container, or production fingerprints.
24
+ - `Bootprint.matrix` as the public convenience API.
25
+ - Stable matrix JSON through `Matrix#to_h`.
26
+
27
+ ### Compatibility
28
+
29
+ - Existing capture, diff, diagnosis, policy, report, CLI, Rails, and plugin APIs are unchanged.
30
+ - Generated timestamps, environment labels, and capture metadata remain excluded from semantic comparisons.
31
+
5
32
  ## 0.2.0 - 2026-08-02
6
33
 
7
34
  ### Added
data/README.md CHANGED
@@ -8,7 +8,9 @@
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://github.com/theworker02/bootprint/actions/workflows/test.yml"><img alt="Test status" src="https://github.com/theworker02/bootprint/actions/workflows/test.yml/badge.svg"></a>
11
- <img alt="Bootprint 0.2.0" src="https://img.shields.io/badge/version-0.2.0-CC342D">
11
+ <a href="https://rubygems.org/gems/bootprint"><img alt="Bootprint on RubyGems" src="https://img.shields.io/gem/v/bootprint?logo=rubygems&logoColor=white&color=CC342D"></a>
12
+ <a href="https://open-vsx.org/extension/theworker02/bootprint"><img alt="Bootprint on Open VSX" src="https://img.shields.io/open-vsx/v/theworker02/bootprint?label=Open%20VSX&color=6C4FBB"></a>
13
+ <a href="https://theworker02.github.io/bootprint/"><img alt="Bootprint website" src="https://img.shields.io/badge/website-GitHub%20Pages-3977F6?logo=githubpages&logoColor=white"></a>
12
14
  <img alt="Ruby 3.1 or newer" src="https://img.shields.io/badge/Ruby-%E2%89%A5%203.1-CC342D?logo=ruby&logoColor=white">
13
15
  <img alt="Snapshot schema version 2" src="https://img.shields.io/badge/snapshot_schema-v2-3977F6">
14
16
  <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-171A21"></a>
@@ -16,6 +18,10 @@
16
18
 
17
19
  Bootprint is a local-first Ruby runtime fingerprint and compatibility diagnostic. It captures a sanitized description of an application environment, compares that description with CI, Docker, staging, or production, and turns raw drift into explanations, severity, evidence, remediation, and enforceable policy.
18
20
 
21
+ Explore the project at [theworker02.github.io/bootprint](https://theworker02.github.io/bootprint/).
22
+
23
+ Install the official [`bootprint` gem from RubyGems.org](https://rubygems.org/gems/bootprint):
24
+
19
25
  ```console
20
26
  gem install bootprint
21
27
  bootprint capture local
@@ -23,6 +29,8 @@ bootprint docker capture myapp:latest
23
29
  bootprint diagnose local myapp-latest
24
30
  ```
25
31
 
32
+ The official editor extension is available from [Open VSX as `theworker02.bootprint`](https://open-vsx.org/extension/theworker02/bootprint).
33
+
26
34
  Bootprint 0.2 combines a dependency compatibility analyzer, Rails boot inspector, environment-drift detector, and CI policy engine. Ordinary capture performs no network requests, suggested commands never execute automatically, and environment-variable values are never recorded.
27
35
 
28
36
  ## Contents
@@ -40,6 +48,7 @@ Bootprint 0.2 combines a dependency compatibility analyzer, Rails boot inspector
40
48
  - [Rules and plugins](#rules-and-plugins)
41
49
  - [Privacy and security](#privacy-and-security)
42
50
  - [Platform support and performance](#platform-support-and-performance)
51
+ - [VS Code extension](#vs-code-extension)
43
52
  - [Documentation](#documentation)
44
53
  - [Development](#development)
45
54
 
@@ -386,6 +395,34 @@ Bootprint is designed around these practical limits:
386
395
 
387
396
  Rails, Docker, SARIF, and plugin code are lazy-loaded so basic CLI use does not pay for integrations it does not invoke. Performance varies with Ruby, filesystem, dependency count, and host load.
388
397
 
398
+ ## VS Code extension
399
+
400
+ Bootprint includes an Open VSX-ready editor extension in [`editors/vscode`](editors/vscode), available from the official [`theworker02.bootprint` Open VSX listing](https://open-vsx.org/extension/theworker02/bootprint). It provides capture, diagnose, doctor, and verify commands, streams CLI output inside the editor, and adds snapshot diagnosis to the Explorer context menu.
401
+
402
+ Install it from an Open VSX-compatible editor or with VSCodium:
403
+
404
+ ```console
405
+ codium --install-extension theworker02.bootprint
406
+ ```
407
+
408
+ The extension uses the workspace bundle by default:
409
+
410
+ ```ruby
411
+ gem "bootprint", "~> 0.2"
412
+ ```
413
+
414
+ Build and install the versioned VSIX locally:
415
+
416
+ ```console
417
+ cd editors/vscode
418
+ npm install
419
+ npm run check
420
+ npm run package:vsix
421
+ code --install-extension ../../pkg/bootprint-vscode-0.2.1.vsix --force
422
+ ```
423
+
424
+ See the [editor extension guide](editors/vscode/README.md) for commands, configuration, trust behavior, Open VSX setup, and direct-executable setup.
425
+
389
426
  ## Documentation
390
427
 
391
428
  - [Installation](docs/installation.md)
@@ -403,6 +440,8 @@ Rails, Docker, SARIF, and plugin code are lazy-loaded so basic CLI use does not
403
440
  - [Snapshot schema](docs/snapshot-schema.md)
404
441
  - [Troubleshooting](docs/troubleshooting.md)
405
442
  - [Maintainer setup](docs/maintainer-setup.md)
443
+ - [VS Code extension](editors/vscode/README.md)
444
+ - [Open VSX publishing](docs/open-vsx.md)
406
445
 
407
446
  ## Development
408
447
 
data/RELEASE.md CHANGED
@@ -54,8 +54,8 @@ The tag starts `.github/workflows/release.yml`, which:
54
54
  2. runs tests and RuboCop;
55
55
  3. builds, installs, and smoke-tests a preflight `.gem` from the tagged source;
56
56
  4. pauses at the protected `release` environment;
57
- 5. rebuilds and publishes through the official RubyGems OIDC action;
58
- 6. prints the published package's SHA-256 digest; and
57
+ 5. prints the release package's SHA-256 digest;
58
+ 6. rebuilds and publishes through the official RubyGems OIDC action; and
59
59
  7. creates a GitHub release with that same package attached.
60
60
 
61
61
  For 0.2.0, use the release title `Bootprint 0.2 — From Environment Differences to Actionable Diagnoses` when editing the generated GitHub release notes.
@@ -71,6 +71,22 @@ gem owner bootprint
71
71
 
72
72
  Confirm the RubyGems page shows the correct links, MIT license, Ruby requirement, MFA requirement, checksum, owners, and trusted publisher. Confirm the GitHub release attachment has the same SHA-256 digest as the workflow output.
73
73
 
74
+ ## Build and publish the Open VSX package
75
+
76
+ Keep `editors/vscode/package.json` aligned with the intended extension release version. The extension depends on the Ruby gem at runtime but is versioned and distributed independently.
77
+
78
+ ```console
79
+ cd editors/vscode
80
+ npm ci
81
+ npm run check
82
+ npm run package:vsix
83
+ code --install-extension ../../pkg/bootprint-vscode-VERSION.vsix
84
+ ```
85
+
86
+ Before distributing the VSIX, inspect its contents with `vsce ls`, verify the bundled license and logo, and smoke-test capture and doctor commands in both Bundler and direct-executable modes. Follow [docs/open-vsx.md](docs/open-vsx.md) for the one-time `theworker02` namespace and protected token setup.
87
+
88
+ Use the manual **Open VSX** workflow to build an artifact without publishing. Set its `publish` input only for an intentional registry release from a reviewed ref. Open VSX versions are immutable, so increment the extension version before every subsequent publication.
89
+
74
90
  ## Emergency and manual release policy
75
91
 
76
92
  Prefer rerunning a failed trusted-publishing job. A manual `gem push` is an emergency fallback only and must use an MFA-protected, least-privilege RubyGems API key from an approved maintainer workstation. Never store that key, an OTP, or a signing private key in the repository, workflow variables, shell history, or issue attachments.
@@ -0,0 +1,5 @@
1
+ {
2
+ "schema_version": 1,
3
+ "generated_at": "2026-08-11T00:00:00Z",
4
+ "advisories": []
5
+ }
@@ -5,10 +5,10 @@ This checklist covers settings that cannot be represented completely by committe
5
5
  ## Repository profile
6
6
 
7
7
  - Description: `Diagnose why Ruby environments work locally but fail in CI, Docker, staging, or production.`
8
- - Website: `https://rubygems.org/gems/bootprint`
8
+ - Website: `https://theworker02.github.io/bootprint/`
9
9
  - Topics: `ruby`, `rails`, `bundler`, `rubygems`, `docker`, `ci`, `diagnostics`, `reproducibility`, `developer-tools`
10
10
  - Default branch: `main`
11
- - Social preview: `assets/branding/bootprint-logo-512.png`
11
+ - Social preview: `site/assets/bootprint-social.png`
12
12
  - Enable Issues and Discussions; disable the wiki unless maintainers intend to support it.
13
13
 
14
14
  ## Ruleset for `main`
@@ -24,6 +24,16 @@ This checklist covers settings that cannot be represented completely by committe
24
24
 
25
25
  CodeQL runs weekly or on explicit maintainer request rather than adding another check to every pull request.
26
26
 
27
+ ## GitHub Pages
28
+
29
+ In **Settings → Pages**, set the publishing source to **GitHub Actions**. The focused `pages.yml` workflow deploys only the dependency-free files under `site/` when that directory changes or when a maintainer runs it manually.
30
+
31
+ - Expected URL: `https://theworker02.github.io/bootprint/`
32
+ - Deployment environment: `github-pages`
33
+ - Allow deployments only from `main`.
34
+ - Set the repository website field to the Pages URL after the first successful deployment.
35
+ - Do not add a custom domain or `CNAME` until its DNS ownership is verified.
36
+
27
37
  ## Actions and security
28
38
 
29
39
  - Allow GitHub-authored actions plus `ruby/setup-ruby` and `rubygems/release-gem`.
data/docs/matrix.md ADDED
@@ -0,0 +1,28 @@
1
+ # Multi-environment matrix
2
+
3
+ Bootprint 0.3.0 can compare a fleet of named snapshots instead of only a local/target pair.
4
+
5
+ ```ruby
6
+ matrix = Bootprint.matrix(
7
+ local: Bootprint::Snapshot.load("tmp/local.json"),
8
+ ci: Bootprint::Snapshot.load("tmp/ci.json"),
9
+ staging: Bootprint::Snapshot.load("tmp/staging.json"),
10
+ production: Bootprint::Snapshot.load("tmp/production.json")
11
+ )
12
+
13
+ matrix.entries.each do |entry|
14
+ puts "#{entry.path}: consensus=#{entry.consensus.inspect} outliers=#{entry.outliers.join(',')}"
15
+ end
16
+ ```
17
+
18
+ A matrix entry contains:
19
+
20
+ - the deterministic dotted path;
21
+ - values by environment;
22
+ - environments where the path is missing;
23
+ - a consensus value when a strict majority agrees;
24
+ - the environments outside that consensus.
25
+
26
+ `outlier_counts` ranks environments by the number of divergent paths. When no strict majority exists, every environment is reported as an outlier for that path instead of guessing which value is correct.
27
+
28
+ Bootprint excludes the same non-semantic timestamps, labels, and capture metadata used by pairwise diffing.
data/docs/open-vsx.md ADDED
@@ -0,0 +1,45 @@
1
+ # Open VSX publishing
2
+
3
+ Bootprint's editor extension uses the Open VSX identifier [`theworker02.bootprint`](https://open-vsx.org/extension/theworker02/bootprint). Open VSX publication is separate from RubyGems and GitHub Releases.
4
+
5
+ ## One-time account setup
6
+
7
+ 1. Create an [Eclipse account](https://accounts.eclipse.org/user/register) and set its GitHub username to `theworker02`.
8
+ 2. Sign in to [Open VSX](https://open-vsx.org/) with that same GitHub account.
9
+ 3. From the Open VSX profile, connect the Eclipse account, read, and accept the Publisher Agreement.
10
+ 4. Generate a dedicated CI access token from [Open VSX Access Tokens](https://open-vsx.org/user-settings/tokens). Copy it immediately; Open VSX will not show it again.
11
+ 5. Load the token from a password manager into the trusted workstation's process environment as `OVSX_PAT`, then create the namespace once without placing the token in the command arguments:
12
+
13
+ ```console
14
+ npx ovsx create-namespace theworker02
15
+ ```
16
+
17
+ 6. Claim ownership of `theworker02` through the public process documented in the [Open VSX namespace guide](https://github.com/eclipse-openvsx/openvsx/wiki/Namespace-Access). Creating a namespace permits publishing, but claiming it is required for the registry's verified-owner indicator.
18
+
19
+ Never commit, paste into an issue, or store the token in ordinary project configuration.
20
+
21
+ ## GitHub setup
22
+
23
+ 1. Create a GitHub environment named `open-vsx`.
24
+ 2. Add an environment secret named `OVSX_PAT` containing a dedicated Open VSX CI token.
25
+ 3. Add required reviewers to the environment so publication requires explicit approval.
26
+ 4. Restrict deployment branches or tags to reviewed release refs.
27
+
28
+ The [Open VSX workflow](../.github/workflows/open-vsx.yml) is manual-only. Running it with `publish` disabled builds and retains a VSIX artifact for review. Running it with `publish` enabled enters the protected environment and uploads the already-tested package.
29
+
30
+ ## Local release gate
31
+
32
+ ```console
33
+ cd editors/vscode
34
+ npm ci
35
+ npm run check
36
+ npm run package:vsix
37
+ ```
38
+
39
+ Inspect `pkg/bootprint-vscode-VERSION.vsix`, install it into an Open VSX-compatible editor, and test capture, diagnosis, doctor, and verify before publishing. For an intentional manual fallback, provide `OVSX_PAT` only through the process environment and run:
40
+
41
+ ```console
42
+ npm run publish:openvsx
43
+ ```
44
+
45
+ Do not publish the same version twice. Update `editors/vscode/package.json`, the lockfile, changelog, and documentation before publishing the next version.
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "advisory_bundle_matcher"
5
+
6
+ module Bootprint
7
+ # Matches snapshot gem versions against an offline advisory bundle.
8
+ class Advisories
9
+ DEFAULT_BUNDLE = File.expand_path("../../data/advisories/schema/empty.json", __dir__).freeze
10
+
11
+ Advisory = Struct.new(:id, :gem, :title, :severity, :affected_versions, :url, :cve, keyword_init: true) do
12
+ def to_h
13
+ {
14
+ "id" => id,
15
+ "gem" => gem,
16
+ "title" => title,
17
+ "severity" => severity,
18
+ "affected_versions" => affected_versions,
19
+ "url" => url,
20
+ "cve" => cve
21
+ }.compact
22
+ end
23
+ end
24
+
25
+ Match = Struct.new(:gem, :version, :advisory, keyword_init: true) do
26
+ def to_h
27
+ {
28
+ "gem" => gem,
29
+ "version" => version,
30
+ "advisory" => advisory.to_h
31
+ }
32
+ end
33
+ end
34
+
35
+ attr_reader :bundle
36
+
37
+ def self.load(path = DEFAULT_BUNDLE)
38
+ parsed = JSON.parse(File.read(path, encoding: "UTF-8"))
39
+ new(parsed, path:)
40
+ rescue JSON::ParserError => error
41
+ raise InvalidSnapshotError, "#{File.expand_path(path)} is not valid advisory JSON: #{error.message}"
42
+ rescue Errno::ENOENT
43
+ raise InvalidSnapshotError, "Advisory bundle not found: #{File.expand_path(path)}"
44
+ end
45
+
46
+ def self.match(snapshot, bundle: DEFAULT_BUNDLE)
47
+ advisories(bundle).matches(snapshot)
48
+ end
49
+
50
+ def self.advisories(bundle = DEFAULT_BUNDLE)
51
+ case bundle
52
+ when Advisories then bundle
53
+ when Hash then new(bundle)
54
+ else load(bundle)
55
+ end
56
+ end
57
+
58
+ def initialize(bundle, path: nil)
59
+ @bundle = normalize_bundle(bundle)
60
+ @path = path
61
+ end
62
+
63
+ def matches(snapshot)
64
+ gems = extract_gems(snapshot)
65
+ bundle_advisories.flat_map do |advisory|
66
+ installed = gems[advisory.gem]
67
+ next [] unless installed
68
+
69
+ version = installed.is_a?(Hash) ? installed["version"] : installed
70
+ next [] unless version && AdvisoryBundleMatcher.version_satisfies?(version, advisory.affected_versions)
71
+
72
+ Match.new(gem: advisory.gem, version: version.to_s, advisory:)
73
+ end
74
+ end
75
+
76
+ def clean?(snapshot) = matches(snapshot).empty?
77
+
78
+ def to_h(snapshot = nil)
79
+ matches = snapshot ? matches(snapshot) : []
80
+ {
81
+ "schema_version" => bundle.fetch("schema_version", 1),
82
+ "bundle" => @path || "inline",
83
+ "clean" => matches.empty?,
84
+ "matches" => matches.map(&:to_h)
85
+ }
86
+ end
87
+
88
+ private
89
+
90
+ def bundle_advisories
91
+ Array(bundle["advisories"]).map do |entry|
92
+ Advisory.new(
93
+ id: entry.fetch("id"),
94
+ gem: entry.fetch("gem"),
95
+ title: entry["title"] || entry.fetch("id"),
96
+ severity: entry["severity"] || "unknown",
97
+ affected_versions: AdvisoryBundleMatcher.normalize_versions(entry["affected_versions"] || entry["versions"]),
98
+ url: entry["url"],
99
+ cve: entry["cve"]
100
+ )
101
+ end
102
+ end
103
+
104
+ def extract_gems(snapshot)
105
+ data = snapshot.is_a?(Snapshot) ? snapshot.data : snapshot
106
+ gems = data.dig("environment", "dependencies", "gems") || {}
107
+ gems.transform_keys(&:to_s)
108
+ end
109
+
110
+ def normalize_bundle(bundle)
111
+ case bundle
112
+ when Hash then bundle.transform_keys(&:to_s)
113
+ when String then self.class.load(bundle).bundle
114
+ else raise ArgumentError, "bundle must be a Hash or path"
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+
5
+ module Bootprint
6
+ module AdvisoryBundleMatcher
7
+ module_function
8
+
9
+ def normalize_versions(value)
10
+ case value
11
+ when nil then []
12
+ when Array then value.map(&:to_s)
13
+ else [value.to_s]
14
+ end
15
+ end
16
+
17
+ def version_satisfies?(version, constraints)
18
+ constraints = normalize_versions(constraints)
19
+ return true if constraints.empty?
20
+
21
+ gem_version = Gem::Version.new(version.to_s)
22
+ Gem::Requirement.new(constraints).satisfied_by?(gem_version)
23
+ rescue ArgumentError
24
+ false
25
+ end
26
+
27
+ def value_in_set?(value, allowed)
28
+ case allowed
29
+ when nil then true
30
+ when Array then allowed.any? { |item| values_equal?(value, item) }
31
+ else values_equal?(value, allowed)
32
+ end
33
+ end
34
+
35
+ def values_equal?(left, right)
36
+ left == right || left.to_s == right.to_s
37
+ end
38
+ end
39
+ end
data/lib/bootprint/cli.rb CHANGED
@@ -37,6 +37,7 @@ module Bootprint
37
37
  when "docker" then docker_command
38
38
  when "ci" then ci_command
39
39
  when "security" then security_command
40
+ when "advisories" then advisories_command
40
41
  when "version", "--version", "-v" then version
41
42
  when "help", "--help", "-h", nil then help(EXIT_OK)
42
43
  else
@@ -294,6 +295,34 @@ module Bootprint
294
295
  issues.empty? ? EXIT_OK : EXIT_POLICY
295
296
  end
296
297
 
298
+ def advisories_command
299
+ options = { format: "human", bundle: Bootprint::Advisories::DEFAULT_BUNDLE }
300
+ parser = OptionParser.new do |opts|
301
+ opts.banner = "Usage: bootprint advisories SNAPSHOT [--bundle PATH] [--format human|json]"
302
+ opts.on("--bundle PATH", "Offline advisory bundle JSON") { |value| options[:bundle] = value }
303
+ opts.on("--format FORMAT", %w[human json]) { |value| options[:format] = value }
304
+ end
305
+ return EXIT_OK unless parse_options(parser)
306
+
307
+ reference = @argv.shift or raise OptionParser::MissingArgument, "SNAPSHOT is required"
308
+ reject_extra_arguments!
309
+ snapshot = load_snapshot(reference)
310
+ bundle = Bootprint::Advisories.load(options[:bundle])
311
+ matches = bundle.matches(snapshot)
312
+ if options[:format] == "json"
313
+ @out.puts JSON.pretty_generate(bundle.to_h(snapshot))
314
+ elsif matches.empty?
315
+ @out.puts "Advisory check passed: no known vulnerabilities in bundled advisories."
316
+ else
317
+ @out.puts "Advisory check found #{matches.length} affected gem(s):"
318
+ matches.each do |match|
319
+ advisory = match.advisory
320
+ @out.puts "#{advisory.severity.to_s.upcase} #{match.gem} #{match.version}: #{advisory.title} (#{advisory.id})"
321
+ end
322
+ end
323
+ matches.empty? ? EXIT_OK : EXIT_POLICY
324
+ end
325
+
297
326
  def report_options
298
327
  { format: "human", allows: [], policy: default_policy_path, only: nil, minimum: nil }
299
328
  end
@@ -449,6 +478,7 @@ module Bootprint
449
478
  docker COMMAND Capture, compare, or diagnose a local Docker image
450
479
  ci verify Verify with CI-native annotations
451
480
  security audit Audit a snapshot for sensitive data
481
+ advisories SNAPSHOT Match gems against offline advisories
452
482
 
453
483
  Run `bootprint COMMAND --help` for command-specific options.
454
484
  HELP
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootprint
4
+ # Compares two or more snapshots to find consensus values and outlier environments.
5
+ class Matrix
6
+ # :values is the matrix cell payload; the name mirrors the JSON "values" key.
7
+ Entry = Struct.new(:path, :values, :missing, :consensus, :outliers, keyword_init: true) do # rubocop:disable Lint/StructNewOverride
8
+ def consensus?
9
+ !consensus.nil?
10
+ end
11
+
12
+ def to_h
13
+ {
14
+ "path" => path,
15
+ "values" => values,
16
+ "missing" => missing,
17
+ "consensus" => consensus,
18
+ "outliers" => outliers
19
+ }
20
+ end
21
+ end
22
+
23
+ attr_reader :snapshots
24
+
25
+ def initialize(snapshots)
26
+ @snapshots = snapshots.to_h.transform_keys(&:to_s).transform_values do |snapshot|
27
+ snapshot.is_a?(Snapshot) ? snapshot.semantic_data : snapshot
28
+ end.freeze
29
+ raise ArgumentError, "matrix requires at least two named snapshots" if @snapshots.length < 2
30
+ end
31
+
32
+ def entries
33
+ @entries ||= begin
34
+ flattened = snapshots.transform_values { |data| flatten(data) }
35
+ paths = flattened.values.flat_map(&:keys).uniq.sort.reject { |path| ignored?(path) }
36
+ paths.filter_map do |path|
37
+ values = {}
38
+ missing = []
39
+ flattened.each do |name, data|
40
+ data.key?(path) ? values[name] = data[path] : missing << name
41
+ end
42
+ distinct = values.values.map { |value| canonical(value) }.uniq.length
43
+ next if distinct <= 1 && missing.empty?
44
+
45
+ consensus = consensus_value(values, snapshots.length)
46
+ outliers = if consensus.nil?
47
+ snapshots.keys.sort
48
+ else
49
+ (values.filter_map { |name, value| name unless value == consensus } + missing).sort
50
+ end
51
+ Entry.new(path:, values: values.sort.to_h, missing: missing.sort, consensus:, outliers:)
52
+ end
53
+ end
54
+ end
55
+
56
+ def clean?
57
+ entries.empty?
58
+ end
59
+
60
+ def outlier_counts
61
+ counts = snapshots.keys.to_h { |name| [name, 0] }
62
+ entries.each { |entry| entry.outliers.each { |name| counts[name] += 1 } }
63
+ counts.sort.to_h
64
+ end
65
+
66
+ def to_h
67
+ {
68
+ "schema" => 1,
69
+ "environments" => snapshots.keys.sort,
70
+ "clean" => clean?,
71
+ "outlier_counts" => outlier_counts,
72
+ "entries" => entries.map(&:to_h)
73
+ }
74
+ end
75
+
76
+ private
77
+
78
+ def flatten(value, prefix = nil, output = {})
79
+ if value.is_a?(Hash)
80
+ value.keys.sort_by(&:to_s).each do |original_key|
81
+ key = original_key.to_s
82
+ path = [prefix, key].compact.join(".")
83
+ flatten(value.fetch(original_key), path, output)
84
+ end
85
+ else
86
+ output[prefix] = value
87
+ end
88
+ output
89
+ end
90
+
91
+ def ignored?(path)
92
+ Diff::IGNORED_PATHS.include?(path) || path == "capture" || path.start_with?("capture.")
93
+ end
94
+
95
+ def consensus_value(values, total)
96
+ groups = values.values.group_by { |value| canonical(value) }
97
+ winner = groups.max_by { |key, grouped| [grouped.length, key] }
98
+ return nil unless winner && winner.last.length > total / 2
99
+
100
+ winner.last.first
101
+ end
102
+
103
+ def canonical(value)
104
+ Marshal.dump(value)
105
+ rescue TypeError
106
+ value.inspect
107
+ end
108
+ end
109
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bootprint
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/bootprint.rb CHANGED
@@ -8,6 +8,8 @@ require_relative "bootprint/schema"
8
8
  require_relative "bootprint/plugins"
9
9
  require_relative "bootprint/snapshot"
10
10
  require_relative "bootprint/diff"
11
+ require_relative "bootprint/matrix"
12
+ require_relative "bootprint/advisories"
11
13
  require_relative "bootprint/doctor"
12
14
  require_relative "bootprint/policy"
13
15
 
@@ -28,6 +30,19 @@ module Bootprint
28
30
  def capture(label: nil, **options)
29
31
  Snapshot.capture(label:, **options)
30
32
  end
33
+
34
+ # Compares named snapshots and identifies consensus values and outliers.
35
+ def matrix(snapshots)
36
+ Matrix.new(snapshots)
37
+ end
38
+
39
+ def advisories(snapshot, bundle: Advisories::DEFAULT_BUNDLE)
40
+ Advisories.advisories(bundle).matches(snapshot)
41
+ end
42
+
43
+ def advise(snapshot, bundle: Advisories::DEFAULT_BUNDLE)
44
+ advisories(snapshot, bundle:)
45
+ end
31
46
  end
32
47
  end
33
48
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootprint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Magnexis
@@ -34,6 +34,7 @@ files:
34
34
  - assets/branding/bootprint-logo-512.png
35
35
  - assets/branding/bootprint-logo-64.png
36
36
  - assets/branding/bootprint-logo.png
37
+ - data/advisories/schema/empty.json
37
38
  - docs/capturing.md
38
39
  - docs/ci.md
39
40
  - docs/comparing.md
@@ -42,6 +43,8 @@ files:
42
43
  - docs/findings.md
43
44
  - docs/installation.md
44
45
  - docs/maintainer-setup.md
46
+ - docs/matrix.md
47
+ - docs/open-vsx.md
45
48
  - docs/plugins.md
46
49
  - docs/policy.md
47
50
  - docs/privacy.md
@@ -51,6 +54,8 @@ files:
51
54
  - docs/troubleshooting.md
52
55
  - exe/bootprint
53
56
  - lib/bootprint.rb
57
+ - lib/bootprint/advisories.rb
58
+ - lib/bootprint/advisory_bundle_matcher.rb
54
59
  - lib/bootprint/analysis.rb
55
60
  - lib/bootprint/cli.rb
56
61
  - lib/bootprint/collectors/environment.rb
@@ -73,6 +78,7 @@ files:
73
78
  - lib/bootprint/formatters/markdown.rb
74
79
  - lib/bootprint/formatters/sarif.rb
75
80
  - lib/bootprint/initializer_profiler.rb
81
+ - lib/bootprint/matrix.rb
76
82
  - lib/bootprint/plugins.rb
77
83
  - lib/bootprint/policy.rb
78
84
  - lib/bootprint/rails_state.rb
@@ -88,12 +94,12 @@ files:
88
94
  - lib/bootprint/snapshot.rb
89
95
  - lib/bootprint/version.rb
90
96
  - lib/tasks/bootprint.rake
91
- homepage: https://github.com/theworker02/bootprint
97
+ homepage: https://theworker02.github.io/bootprint/
92
98
  licenses:
93
99
  - MIT
94
100
  metadata:
95
101
  source_code_uri: https://github.com/theworker02/bootprint/tree/main
96
- homepage_uri: https://github.com/theworker02/bootprint
102
+ homepage_uri: https://theworker02.github.io/bootprint/
97
103
  documentation_uri: https://github.com/theworker02/bootprint/tree/main/docs
98
104
  changelog_uri: https://github.com/theworker02/bootprint/blob/main/CHANGELOG.md
99
105
  bug_tracker_uri: https://github.com/theworker02/bootprint/issues