bootprint 0.2.0 → 0.5.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: 84278443cdd38e08c4b03bdd0776b8a9b7fd32a623b5ebf2f2c3e1ddc29901c2
4
+ data.tar.gz: e51b0d1b314473e6acb15544ab81a9fbd09de46994f929eff9b5751769db3f80
5
5
  SHA512:
6
- metadata.gz: ae4e15f6cd2655c988c3f7b845d417240e433b8e1384c5145b984bc0c243fbb0ad10c5fd09fb93cd8c38570b0682538a459e98db931a77c43c16686bf2963d71
7
- data.tar.gz: 7070a15b2d7a82018cb526c9b0fad201ea8259f87a6daa63641ad5be745fe9eee899348c33f41d1f80879e2a103cb6f3a89444eb65b839ec70660e587b76cc5c
6
+ metadata.gz: e3be35d8bc8b7d970b4239b987fef35361677e8347fc24c200db57118515ebc2be83691c371f892b1d47a4fcf05cfd6b408c453282f117b5d3cec28ee1702c76
7
+ data.tar.gz: 1a99662465a2ef9d867b4e81016d4ce92c0c02ca949d2a6425e30c26ee223739a976f941e12162c432e53dca052f79be668f0ce29ca67a57abf329dba1d5a473
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,40 @@
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.5.0 - 2026-08-14
8
+
9
+ ### Added
10
+
11
+ - Capture timezone, default encodings, and locale metadata on the operating-system fingerprint.
12
+ - Built-in rules `timezone-drift`, `encoding-drift`, and `locale-drift` for those fields.
13
+
14
+ ## 0.4.0 - 2026-08-11
15
+
16
+ ### Added
17
+
18
+ - `Bootprint::Advisories` for matching snapshot gem versions against offline JSON advisory bundles.
19
+ - `Bootprint.advisories` and `Bootprint.advise` convenience APIs.
20
+ - `bootprint advisories SNAPSHOT` CLI command with `--bundle` and `--format human|json` options.
21
+ - Sample empty advisory bundle schema at `data/advisories/schema/empty.json`.
22
+
23
+ ## 0.3.0 - 2026-08-04
24
+
25
+ ### Added
26
+
27
+ - `Bootprint::Matrix` for comparing two or more named snapshots at once.
28
+ - Majority-based consensus values for multi-environment drift analysis.
29
+ - Deterministic outlier identification and per-environment outlier counts.
30
+ - Missing-value reporting for incomplete staging, CI, container, or production fingerprints.
31
+ - `Bootprint.matrix` as the public convenience API.
32
+ - Stable matrix JSON through `Matrix#to_h`.
33
+
34
+ ### Compatibility
35
+
36
+ - Existing capture, diff, diagnosis, policy, report, CLI, Rails, and plugin APIs are unchanged.
37
+ - Generated timestamps, environment labels, and capture metadata remain excluded from semantic comparisons.
38
+
5
39
  ## 0.2.0 - 2026-08-02
6
40
 
7
41
  ### 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
 
@@ -47,7 +56,7 @@ Bootprint 0.2 combines a dependency compatibility analyzer, Rails boot inspector
47
56
 
48
57
  `Gemfile.lock` captures dependency resolution, but not the complete runtime contract. Ruby engine and patch level, native clients, libc, CPU architecture, Rails adapters, required configuration, filesystem behavior, and initializer ordering can all change application behavior.
49
58
 
50
- Bootprint records those facts as deterministic schema-v2 JSON and evaluates them with 39 independently testable built-in rules. A finding answers four questions that a plain diff cannot:
59
+ Bootprint records those facts as deterministic schema-v2 JSON and evaluates them with 42 independently testable built-in rules. A finding answers four questions that a plain diff cannot:
51
60
 
52
61
  1. What changed?
53
62
  2. How dangerous is it?
@@ -70,7 +79,7 @@ ERROR Required environment variable is missing
70
79
 
71
80
  | Capability | Bootprint 0.2 |
72
81
  |---|---|
73
- | Diagnostic knowledge | 39 built-in rules across runtime, dependencies, native libraries, configuration, filesystem, and Rails boot |
82
+ | Diagnostic knowledge | 42 built-in rules across runtime, dependencies, native libraries, configuration, filesystem, locale, and Rails boot |
74
83
  | Severity model | `info`, `warning`, `error`, `critical` |
75
84
  | Report formats | Human terminal output, JSON, SARIF 2.1, Markdown |
76
85
  | Snapshot contract | Deterministic schema v2 with in-memory v1 migration |
@@ -88,6 +97,7 @@ ERROR Required environment variable is missing
88
97
  | Native libraries | OpenSSL, libyaml, SQLite, PostgreSQL, MySQL, libc, compiler and header availability when detectable |
89
98
  | Configuration | Environment-variable names and presence, Rails environment, adapters, framework settings |
90
99
  | Filesystem | Temporary/log path availability, writability, path separators, case sensitivity, symlink behavior |
100
+ | Locale | Timezone name and UTC offset, default internal/external encodings, LANG / LC_ALL / charmap |
91
101
  | Rails boot | Framework configuration, autoload/eager-load paths, initializers, and opt-in initializer timings |
92
102
  | Docker | Image runtime, platforms, installed package metadata when available, workdir, entrypoint, command, permissions |
93
103
 
@@ -386,6 +396,34 @@ Bootprint is designed around these practical limits:
386
396
 
387
397
  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
398
 
399
+ ## VS Code extension
400
+
401
+ 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.
402
+
403
+ Install it from an Open VSX-compatible editor or with VSCodium:
404
+
405
+ ```console
406
+ codium --install-extension theworker02.bootprint
407
+ ```
408
+
409
+ The extension uses the workspace bundle by default:
410
+
411
+ ```ruby
412
+ gem "bootprint", "~> 0.2"
413
+ ```
414
+
415
+ Build and install the versioned VSIX locally:
416
+
417
+ ```console
418
+ cd editors/vscode
419
+ npm install
420
+ npm run check
421
+ npm run package:vsix
422
+ code --install-extension ../../pkg/bootprint-vscode-0.2.1.vsix --force
423
+ ```
424
+
425
+ See the [editor extension guide](editors/vscode/README.md) for commands, configuration, trust behavior, Open VSX setup, and direct-executable setup.
426
+
389
427
  ## Documentation
390
428
 
391
429
  - [Installation](docs/installation.md)
@@ -403,6 +441,8 @@ Rails, Docker, SARIF, and plugin code are lazy-loaded so basic CLI use does not
403
441
  - [Snapshot schema](docs/snapshot-schema.md)
404
442
  - [Troubleshooting](docs/troubleshooting.md)
405
443
  - [Maintainer setup](docs/maintainer-setup.md)
444
+ - [VS Code extension](editors/vscode/README.md)
445
+ - [Open VSX publishing](docs/open-vsx.md)
406
446
 
407
447
  ## Development
408
448
 
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
@@ -407,7 +436,7 @@ module Bootprint
407
436
  raise OptionParser::InvalidArgument, "NAME may contain only letters, numbers, dots, underscores, and hyphens" unless valid
408
437
  end
409
438
 
410
- def safe_name(value) = value.to_s.gsub(/[^a-zA-Z0-9_.-]+/, "-").gsub(/\A-+|-+\z/, "")
439
+ def safe_name(value) = value.to_s.gsub(/[^a-zA-Z0-9_.-]+/, "-").gsub(/\A-++|-++\z/, "")
411
440
 
412
441
  def reject_extra_arguments!
413
442
  raise OptionParser::ParseError, "unexpected arguments: #{@argv.join(' ')}" unless @argv.empty?
@@ -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
@@ -19,7 +19,10 @@ module Bootprint
19
19
  "processors" => Etc.nprocessors,
20
20
  "capabilities" => TOOLS.to_h { |tool| [tool, executable?(tool)] },
21
21
  "ruby_headers" => header_state,
22
- "ci" => ci_provider
22
+ "ci" => ci_provider,
23
+ "timezone" => timezone_state,
24
+ "encoding" => encoding_state,
25
+ "locale" => locale_state
23
26
  }
24
27
  end
25
28
 
@@ -45,6 +48,30 @@ module Bootprint
45
48
 
46
49
  nil
47
50
  end
51
+
52
+ def timezone_state
53
+ now = Time.now
54
+ {
55
+ "name" => now.zone,
56
+ "utc_offset" => now.utc_offset,
57
+ "tz" => ENV.fetch("TZ", nil)
58
+ }
59
+ end
60
+
61
+ def encoding_state
62
+ {
63
+ "external" => Encoding.default_external.name,
64
+ "internal" => Encoding.default_internal&.name
65
+ }
66
+ end
67
+
68
+ def locale_state
69
+ {
70
+ "lang" => ENV.fetch("LANG", nil),
71
+ "lc_all" => ENV.fetch("LC_ALL", nil),
72
+ "charmap" => Encoding.locale_charmap
73
+ }
74
+ end
48
75
  end
49
76
  end
50
77
  end
@@ -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
@@ -58,6 +58,24 @@ module Bootprint
58
58
  fix: "Use a standard release build for production.") do |_source, target|
59
59
  dig(target, "runtime.debug_build") == true && { "description" => dig(target, "runtime.description") }
60
60
  end
61
+ rule("timezone-drift", "Timezone mismatch", :runtime, :warning,
62
+ cause: "The environments select different timezones.",
63
+ impact: "Time.now, cron, and timestamp formatting can disagree across machines.",
64
+ fix: "Set TZ consistently in local, container, and CI environments.") do |source, target|
65
+ difference(source, target, "operating_system.timezone")
66
+ end
67
+ rule("encoding-drift", "Default encoding mismatch", :runtime, :warning,
68
+ cause: "Ruby default encodings differ between environments.",
69
+ impact: "String transcoding, file IO, and JSON payloads can fail only in one environment.",
70
+ fix: "Align Encoding.default_external (and LANG/LC_ALL) with the deployment runtime.") do |source, target|
71
+ difference(source, target, "operating_system.encoding")
72
+ end
73
+ rule("locale-drift", "Locale mismatch", :runtime, :info,
74
+ cause: "LANG / LC_ALL / locale charmap differ between environments.",
75
+ impact: "Sort order, number formatting, and encoding defaults can change.",
76
+ fix: "Export the same LANG and LC_ALL in every environment.") do |source, target|
77
+ difference(source, target, "operating_system.locale")
78
+ end
61
79
  end
62
80
 
63
81
  def dependency_rules
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bootprint
4
- VERSION = "0.2.0"
4
+ VERSION = "0.5.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.5.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