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 +4 -4
- data/ARCHITECTURE.md +2 -0
- data/CHANGELOG.md +34 -0
- data/README.md +43 -3
- data/RELEASE.md +18 -2
- data/data/advisories/schema/empty.json +5 -0
- data/docs/maintainer-setup.md +12 -2
- data/docs/matrix.md +28 -0
- data/docs/open-vsx.md +45 -0
- data/lib/bootprint/advisories.rb +118 -0
- data/lib/bootprint/advisory_bundle_matcher.rb +39 -0
- data/lib/bootprint/cli.rb +31 -1
- data/lib/bootprint/collectors/operating_system.rb +28 -1
- data/lib/bootprint/matrix.rb +109 -0
- data/lib/bootprint/rules/builtin.rb +18 -0
- data/lib/bootprint/version.rb +1 -1
- data/lib/bootprint.rb +15 -0
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 84278443cdd38e08c4b03bdd0776b8a9b7fd32a623b5ebf2f2c3e1ddc29901c2
|
|
4
|
+
data.tar.gz: e51b0d1b314473e6acb15544ab81a9fbd09de46994f929eff9b5751769db3f80
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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 |
|
|
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.
|
|
58
|
-
6.
|
|
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.
|
data/docs/maintainer-setup.md
CHANGED
|
@@ -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://
|
|
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/
|
|
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
|
|
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
|
data/lib/bootprint/version.rb
CHANGED
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.
|
|
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.
|
|
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.
|
|
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
|