bootprint 0.2.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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.bootprint.yml.example +37 -0
  3. data/ARCHITECTURE.md +46 -0
  4. data/CHANGELOG.md +26 -0
  5. data/CODE_OF_CONDUCT.md +7 -0
  6. data/CONTRIBUTING.md +28 -0
  7. data/LICENSE +21 -0
  8. data/README.md +422 -0
  9. data/RELEASE.md +78 -0
  10. data/ROADMAP.md +15 -0
  11. data/SECURITY.md +47 -0
  12. data/assets/branding/README.md +24 -0
  13. data/assets/branding/bootprint-logo-128.png +0 -0
  14. data/assets/branding/bootprint-logo-512.png +0 -0
  15. data/assets/branding/bootprint-logo-64.png +0 -0
  16. data/assets/branding/bootprint-logo.png +0 -0
  17. data/docs/capturing.md +9 -0
  18. data/docs/ci.md +21 -0
  19. data/docs/comparing.md +22 -0
  20. data/docs/custom-rules.md +7 -0
  21. data/docs/docker.md +7 -0
  22. data/docs/findings.md +7 -0
  23. data/docs/installation.md +7 -0
  24. data/docs/maintainer-setup.md +54 -0
  25. data/docs/plugins.md +7 -0
  26. data/docs/policy.md +9 -0
  27. data/docs/privacy.md +7 -0
  28. data/docs/quick-start.md +9 -0
  29. data/docs/rails.md +13 -0
  30. data/docs/snapshot-schema.md +9 -0
  31. data/docs/troubleshooting.md +8 -0
  32. data/exe/bootprint +6 -0
  33. data/lib/bootprint/analysis.rb +13 -0
  34. data/lib/bootprint/cli.rb +458 -0
  35. data/lib/bootprint/collectors/environment.rb +21 -0
  36. data/lib/bootprint/collectors/filesystem.rb +40 -0
  37. data/lib/bootprint/collectors/gems.rb +96 -0
  38. data/lib/bootprint/collectors/libraries.rb +75 -0
  39. data/lib/bootprint/collectors/operating_system.rb +50 -0
  40. data/lib/bootprint/collectors/rails.rb +97 -0
  41. data/lib/bootprint/collectors/runtime.rb +34 -0
  42. data/lib/bootprint/collectors/toolchain.rb +23 -0
  43. data/lib/bootprint/configuration.rb +38 -0
  44. data/lib/bootprint/diagnosis.rb +95 -0
  45. data/lib/bootprint/diff.rb +47 -0
  46. data/lib/bootprint/docker.rb +149 -0
  47. data/lib/bootprint/doctor.rb +13 -0
  48. data/lib/bootprint/errors.rb +9 -0
  49. data/lib/bootprint/formatters/human.rb +55 -0
  50. data/lib/bootprint/formatters/json.rb +12 -0
  51. data/lib/bootprint/formatters/markdown.rb +27 -0
  52. data/lib/bootprint/formatters/sarif.rb +54 -0
  53. data/lib/bootprint/formatters.rb +22 -0
  54. data/lib/bootprint/initializer_profiler.rb +93 -0
  55. data/lib/bootprint/plugins.rb +90 -0
  56. data/lib/bootprint/policy.rb +191 -0
  57. data/lib/bootprint/rails_state.rb +17 -0
  58. data/lib/bootprint/railtie.rb +36 -0
  59. data/lib/bootprint/rules/builtin.rb +383 -0
  60. data/lib/bootprint/rules/finding.rb +40 -0
  61. data/lib/bootprint/rules/registry.rb +20 -0
  62. data/lib/bootprint/rules/rule.rb +153 -0
  63. data/lib/bootprint/rules.rb +58 -0
  64. data/lib/bootprint/sanitizer.rb +105 -0
  65. data/lib/bootprint/schema.rb +111 -0
  66. data/lib/bootprint/security/auditor.rb +63 -0
  67. data/lib/bootprint/snapshot.rb +122 -0
  68. data/lib/bootprint/version.rb +5 -0
  69. data/lib/bootprint.rb +38 -0
  70. data/lib/tasks/bootprint.rake +18 -0
  71. metadata +120 -0
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootprint
4
+ module Sanitizer
5
+ REDACTED = "[REDACTED]"
6
+ JWT_PATTERN = /\AeyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\z/
7
+ PRIVATE_KEY_PATTERN = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/
8
+ URL_CREDENTIAL_PATTERN = %r{\A([a-z][a-z0-9+.-]*://)([^/@\s]+)@}i
9
+ HIGH_ENTROPY_PATTERN = %r{\A[A-Za-z0-9+/_=-]{32,}\z}
10
+
11
+ module_function
12
+
13
+ def text(value)
14
+ sanitized = value.to_s.dup
15
+ replacements.each do |path, marker|
16
+ path_variants(path).each do |variant|
17
+ sanitized.gsub!(variant, marker)
18
+ end
19
+ end
20
+ sanitized.gsub(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/, "?")
21
+ end
22
+
23
+ def path(value)
24
+ text(value)
25
+ end
26
+
27
+ def recursive(value, patterns: Bootprint.configuration.redaction_patterns, privacy: Bootprint.configuration.privacy)
28
+ case value
29
+ when Hash
30
+ value.each_with_object({}) do |(key, nested), result|
31
+ name = key.to_s
32
+ result[name] = if secret_name?(name, patterns) && !boolean?(nested)
33
+ redaction_metadata(nested)
34
+ elsif safe_digest_name?(name) && !nested.is_a?(Hash) && !nested.is_a?(Array)
35
+ nested.to_s
36
+ else
37
+ recursive(nested, patterns:, privacy:)
38
+ end
39
+ end
40
+ when Array
41
+ value.map { |nested| recursive(nested, patterns:, privacy:) }
42
+ when String
43
+ sanitize_value(value, privacy:)
44
+ else
45
+ value
46
+ end
47
+ end
48
+
49
+ def secret_name?(name, patterns = Bootprint.configuration.redaction_patterns)
50
+ return false if Bootprint.configuration.redaction_safe_list.any? { |entry| File.fnmatch?(entry, name, File::FNM_CASEFOLD) }
51
+
52
+ patterns.any? { |pattern| name.upcase.include?(pattern.to_s.upcase) }
53
+ end
54
+
55
+ def safe_digest_name?(name)
56
+ name.match?(/(?:checksum|sha256|digest)\z/i)
57
+ end
58
+
59
+ def boolean?(value)
60
+ [true, false].include?(value)
61
+ end
62
+
63
+ def sensitive_value?(value)
64
+ string = value.to_s
65
+ PRIVATE_KEY_PATTERN.match?(string) || JWT_PATTERN.match?(string) ||
66
+ URL_CREDENTIAL_PATTERN.match?(string) || high_entropy?(string)
67
+ end
68
+
69
+ def sanitize_value(value, privacy: :standard)
70
+ result = text(value)
71
+ result = result.gsub(URL_CREDENTIAL_PATTERN, "\\1#{REDACTED}@")
72
+ result = REDACTED if PRIVATE_KEY_PATTERN.match?(result) || JWT_PATTERN.match?(result) || high_entropy?(result)
73
+ result = "<HOST>" if privacy.to_sym == :strict && hostname_like?(result)
74
+ result
75
+ end
76
+
77
+ def high_entropy?(value)
78
+ return false unless HIGH_ENTROPY_PATTERN.match?(value)
79
+
80
+ value.chars.uniq.length >= 16
81
+ end
82
+
83
+ def hostname_like?(value)
84
+ value.match?(/\A(?=.{1,253}\z)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}\z/i)
85
+ end
86
+
87
+ def redaction_metadata(value)
88
+ { "present" => !value.nil?, "redacted" => true }
89
+ end
90
+
91
+ def replacements
92
+ app_root = File.expand_path(Dir.pwd)
93
+ home = begin
94
+ File.expand_path(Dir.home)
95
+ rescue ArgumentError
96
+ nil
97
+ end
98
+ [[app_root, "<APP_ROOT>"], [home, "<HOME>"]].reject { |path, _marker| path.nil? || path.empty? }
99
+ end
100
+
101
+ def path_variants(path)
102
+ [path, path.tr("\\", "/"), path.tr("/", "\\")].uniq.sort_by { |variant| -variant.length }
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootprint
4
+ module Schema
5
+ CURRENT_VERSION = 2
6
+ ENVIRONMENT_SECTIONS = %w[runtime dependencies native_libraries configuration filesystem operating_system].freeze
7
+
8
+ module_function
9
+
10
+ def migrate(data)
11
+ raise InvalidSnapshotError, "Snapshot root must be a JSON object" unless data.is_a?(Hash)
12
+
13
+ version = data.is_a?(Hash) ? data["schema_version"] || data[:schema_version] : nil
14
+ case version
15
+ when CURRENT_VERSION then stringify(data)
16
+ when 1 then migrate_v1(stringify(data))
17
+ when nil then raise InvalidSnapshotError, "Snapshot does not declare schema_version"
18
+ else
19
+ if version.is_a?(Integer) && version > CURRENT_VERSION
20
+ raise InvalidSnapshotError, "Snapshot schema #{version} is newer than supported schema #{CURRENT_VERSION}; upgrade Bootprint"
21
+ end
22
+
23
+ raise InvalidSnapshotError, "Unsupported snapshot schema #{version.inspect}"
24
+ end
25
+ end
26
+
27
+ def validate!(data)
28
+ raise InvalidSnapshotError, "Snapshot root must be a JSON object" unless data.is_a?(Hash)
29
+ raise InvalidSnapshotError, "schema_version must equal #{CURRENT_VERSION}" unless data["schema_version"] == CURRENT_VERSION
30
+ raise InvalidSnapshotError, "generated_at must be an ISO-8601 string" unless data["generated_at"].is_a?(String)
31
+ raise InvalidSnapshotError, "bootprint_version must be a string" unless data["bootprint_version"].is_a?(String)
32
+
33
+ environment = data["environment"]
34
+ raise InvalidSnapshotError, "environment must be an object" unless environment.is_a?(Hash)
35
+
36
+ missing = ENVIRONMENT_SECTIONS - environment.keys
37
+ raise InvalidSnapshotError, "environment is missing sections: #{missing.join(', ')}" unless missing.empty?
38
+
39
+ invalid = ENVIRONMENT_SECTIONS.reject { |key| environment[key].is_a?(Hash) }
40
+ raise InvalidSnapshotError, "environment sections must be objects: #{invalid.join(', ')}" unless invalid.empty?
41
+
42
+ variables = environment.dig("configuration", "environment_variables")
43
+ unless variables.is_a?(Hash) && variables.values.all? { |value| [true, false].include?(value) }
44
+ raise InvalidSnapshotError, "configuration.environment_variables must map names to booleans"
45
+ end
46
+
47
+ true
48
+ end
49
+
50
+ def migrate_v1(old)
51
+ known = %w[schema_version metadata runtime toolchain gems libraries environment operating_system rails]
52
+ unknown = old.except(*known)
53
+ rails = old["rails"] || {}
54
+ {
55
+ "schema_version" => CURRENT_VERSION,
56
+ "generated_at" => old.dig("metadata", "captured_at") || Time.now.utc.iso8601,
57
+ "bootprint_version" => VERSION,
58
+ "environment" => {
59
+ "name" => old.dig("metadata", "label"),
60
+ "runtime" => old["runtime"] || {},
61
+ "dependencies" => {
62
+ "toolchain" => old["toolchain"] || {},
63
+ "gems" => old.dig("gems", "resolved") || {},
64
+ "lockfile" => { "sha256" => old.dig("gems", "lockfile_sha256"), "platforms" => [] }.compact
65
+ },
66
+ "native_libraries" => old["libraries"] || {},
67
+ "configuration" => {
68
+ "environment_variables" => old.dig("environment", "variables") || {},
69
+ "required_environment_variables" => [],
70
+ "optional_environment_variables" => [],
71
+ "rails" => rails
72
+ },
73
+ "filesystem" => default_filesystem,
74
+ "operating_system" => old["operating_system"] || {}
75
+ }.compact,
76
+ "capture" => { "migrated_from" => 1 },
77
+ "extensions" => unknown
78
+ }
79
+ end
80
+
81
+ def default_filesystem
82
+ {
83
+ "path_separator" => File::ALT_SEPARATOR || File::SEPARATOR,
84
+ "temporary_directory" => { "present" => true, "writable" => true },
85
+ "required_directories" => {}
86
+ }
87
+ end
88
+
89
+ def stringify(value)
90
+ case value
91
+ when Hash then value.to_h { |key, nested| [key.to_s, stringify(nested)] }
92
+ when Array then value.map { |nested| stringify(nested) }
93
+ else value
94
+ end
95
+ end
96
+
97
+ def deterministic(value)
98
+ case value
99
+ when Hash then value.keys.sort.to_h { |key| [key, deterministic(value[key])] }
100
+ when Array
101
+ normalized = value.map { |nested| deterministic(nested) }
102
+ normalized.all? { |item| scalar?(item) } ? normalized.sort_by(&:to_s) : normalized
103
+ else value
104
+ end
105
+ end
106
+
107
+ def scalar?(value)
108
+ value.nil? || value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootprint
4
+ module Security
5
+ class Auditor
6
+ Issue = Struct.new(:path, :kind, :message, keyword_init: true) do
7
+ def to_h = { "path" => path, "kind" => kind, "message" => message }
8
+ end
9
+
10
+ def initialize(snapshot)
11
+ @snapshot = snapshot
12
+ end
13
+
14
+ def audit
15
+ issues = []
16
+ walk(@snapshot.data, [], issues)
17
+ issues
18
+ end
19
+
20
+ private
21
+
22
+ def walk(value, path, issues)
23
+ case value
24
+ when Hash
25
+ value.each do |key, nested|
26
+ current = path + [key]
27
+ if Sanitizer.secret_name?(key) && !presence_boolean?(nested) && !redacted_metadata?(nested)
28
+ issues << Issue.new(path: current.join("."), kind: "secret-name",
29
+ message: "Secret-like field is not represented by redaction metadata.")
30
+ end
31
+ walk(nested, current, issues)
32
+ end
33
+ when Array
34
+ value.each_with_index { |nested, index| walk(nested, path + [index], issues) }
35
+ when String
36
+ return if Sanitizer.safe_digest_name?(path.last.to_s)
37
+
38
+ if Sanitizer.sensitive_value?(value)
39
+ issues << Issue.new(path: path.join("."), kind: "sensitive-value",
40
+ message: "Value resembles a credential, token, private key, or credential-bearing URL.")
41
+ elsif absolute_home_path?(value)
42
+ issues << Issue.new(path: path.join("."), kind: "home-path", message: "Absolute user-home path was not normalized.")
43
+ end
44
+ end
45
+ end
46
+
47
+ def redacted_metadata?(value)
48
+ value.is_a?(Hash) && value["redacted"] == true && value.keys.all? { |key| %w[present source redacted].include?(key) }
49
+ end
50
+
51
+ def presence_boolean?(value)
52
+ [true, false].include?(value)
53
+ end
54
+
55
+ def absolute_home_path?(value)
56
+ home = File.expand_path(Dir.home)
57
+ value.include?(home) || value.include?(home.tr("\\", "/"))
58
+ rescue ArgumentError
59
+ false
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require "fileutils"
6
+ require_relative "collectors/runtime"
7
+ require_relative "collectors/toolchain"
8
+ require_relative "collectors/gems"
9
+ require_relative "collectors/libraries"
10
+ require_relative "collectors/environment"
11
+ require_relative "collectors/filesystem"
12
+ require_relative "collectors/operating_system"
13
+ require_relative "collectors/rails"
14
+
15
+ module Bootprint
16
+ class Snapshot
17
+ SCHEMA_VERSION = Schema::CURRENT_VERSION
18
+ NON_SEMANTIC_PATHS = %w[generated_at capture.duration_ms capture.plugins].freeze
19
+
20
+ attr_reader :data
21
+
22
+ def self.capture(label: nil, privacy: Bootprint.configuration.privacy)
23
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
24
+ Bootprint.configuration.privacy = privacy.to_sym
25
+ warnings = []
26
+ environment = {
27
+ "name" => label,
28
+ "runtime" => safely_collect(Collectors::Runtime, warnings),
29
+ "dependencies" => dependency_data(warnings),
30
+ "native_libraries" => safely_collect(Collectors::Libraries, warnings),
31
+ "configuration" => configuration_data(warnings),
32
+ "filesystem" => safely_collect(Collectors::Filesystem, warnings),
33
+ "operating_system" => safely_collect(Collectors::OperatingSystem, warnings)
34
+ }.compact
35
+
36
+ plugin_data = if defined?(Bootprint::Plugins)
37
+ Bootprint::Plugins.capture(warnings:, strict: Bootprint.configuration.plugin_strict)
38
+ else
39
+ {}
40
+ end
41
+ environment["plugins"] = plugin_data unless plugin_data.empty?
42
+ duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1_000
43
+ new({
44
+ "schema_version" => SCHEMA_VERSION,
45
+ "generated_at" => Time.now.utc.iso8601,
46
+ "bootprint_version" => VERSION,
47
+ "environment" => Sanitizer.recursive(environment, privacy:),
48
+ "capture" => {
49
+ "duration_ms" => duration.round(3),
50
+ "privacy" => privacy.to_s,
51
+ "warnings" => warnings
52
+ }
53
+ })
54
+ end
55
+
56
+ def self.load(path)
57
+ parsed = JSON.parse(File.read(path, encoding: "UTF-8"))
58
+ new(parsed)
59
+ rescue JSON::ParserError => error
60
+ raise InvalidSnapshotError, "#{File.expand_path(path)} is not valid JSON: #{error.message}"
61
+ rescue Errno::ENOENT
62
+ raise InvalidSnapshotError, "Snapshot not found: #{File.expand_path(path)}"
63
+ end
64
+
65
+ def self.safely_collect(collector, warnings)
66
+ collector.capture
67
+ rescue StandardError => error
68
+ warnings << {
69
+ "collector" => collector.key,
70
+ "message" => Sanitizer.text("#{error.class}: #{error.message}"),
71
+ "severity" => "warning"
72
+ }
73
+ { "capture_error" => Sanitizer.text("#{error.class}: #{error.message}") }
74
+ end
75
+
76
+ def self.dependency_data(warnings)
77
+ gems = safely_collect(Collectors::Gems, warnings)
78
+ {
79
+ "toolchain" => safely_collect(Collectors::Toolchain, warnings),
80
+ "gems" => gems["resolved"] || {},
81
+ "lockfile" => gems.except("resolved")
82
+ }
83
+ end
84
+
85
+ def self.configuration_data(warnings)
86
+ environment = safely_collect(Collectors::Environment, warnings)
87
+ rails = safely_collect(Collectors::Rails, warnings)
88
+ {
89
+ "environment_variables" => environment["variables"] || {},
90
+ "required_environment_variables" => Bootprint.configuration.required_environment_names.sort,
91
+ "optional_environment_variables" => Bootprint.configuration.optional_environment_names.sort,
92
+ "rails" => rails || {}
93
+ }
94
+ end
95
+
96
+ def initialize(data)
97
+ migrated = Schema.migrate(Schema.stringify(data))
98
+ Schema.validate!(migrated)
99
+ @data = Schema.deterministic(migrated)
100
+ end
101
+
102
+ def write(path)
103
+ directory = File.dirname(File.expand_path(path))
104
+ FileUtils.mkdir_p(directory)
105
+ File.write(path, "#{JSON.pretty_generate(data)}\n")
106
+ path
107
+ end
108
+
109
+ def [](key) = data[key.to_s]
110
+ def environment = data["environment"]
111
+ def name = environment["name"] || "unnamed"
112
+
113
+ def semantic_data
114
+ Marshal.load(Marshal.dump(data)).tap do |copy|
115
+ copy.delete("generated_at")
116
+ copy.delete("bootprint_version")
117
+ copy["capture"]&.delete("duration_ms")
118
+ copy["capture"]&.delete("warnings")
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bootprint
4
+ VERSION = "0.2.0"
5
+ end
data/lib/bootprint.rb ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "bootprint/version"
4
+ require_relative "bootprint/errors"
5
+ require_relative "bootprint/configuration"
6
+ require_relative "bootprint/sanitizer"
7
+ require_relative "bootprint/schema"
8
+ require_relative "bootprint/plugins"
9
+ require_relative "bootprint/snapshot"
10
+ require_relative "bootprint/diff"
11
+ require_relative "bootprint/doctor"
12
+ require_relative "bootprint/policy"
13
+
14
+ module Bootprint
15
+ class << self
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def configure
21
+ yield(configuration)
22
+ end
23
+
24
+ def rule(name, severity: nil, &block)
25
+ Rules.define(name, severity: severity, &block)
26
+ end
27
+
28
+ def capture(label: nil, **options)
29
+ Snapshot.capture(label:, **options)
30
+ end
31
+ end
32
+ end
33
+
34
+ require_relative "bootprint/rules"
35
+ require_relative "bootprint/diagnosis"
36
+ require_relative "bootprint/analysis"
37
+
38
+ require_relative "bootprint/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :bootprint do
4
+ desc "Capture a sanitized Rails runtime fingerprint"
5
+ task capture: :environment do
6
+ require "bootprint"
7
+ path = ENV.fetch("BOOTPRINT_OUTPUT", "bootprint.lock")
8
+ Bootprint.capture(label: Rails.env.to_s).write(path)
9
+ puts "Captured Bootprint snapshot to #{path}"
10
+ end
11
+
12
+ desc "Run Bootprint Rails runtime diagnostics"
13
+ task doctor: :environment do
14
+ require "bootprint/cli"
15
+ exit_code = Bootprint::CLI.start(["doctor"])
16
+ abort "Bootprint doctor found blocking issues" unless exit_code.zero?
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bootprint
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Magnexis
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Bootprint captures sanitized Ruby and Rails runtime fingerprints, diagnoses
13
+ dangerous environment drift, and provides policy-aware remediation for CI, Docker,
14
+ staging, and production.
15
+ email:
16
+ - hello@magnexis.com
17
+ executables:
18
+ - bootprint
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".bootprint.yml.example"
23
+ - ARCHITECTURE.md
24
+ - CHANGELOG.md
25
+ - CODE_OF_CONDUCT.md
26
+ - CONTRIBUTING.md
27
+ - LICENSE
28
+ - README.md
29
+ - RELEASE.md
30
+ - ROADMAP.md
31
+ - SECURITY.md
32
+ - assets/branding/README.md
33
+ - assets/branding/bootprint-logo-128.png
34
+ - assets/branding/bootprint-logo-512.png
35
+ - assets/branding/bootprint-logo-64.png
36
+ - assets/branding/bootprint-logo.png
37
+ - docs/capturing.md
38
+ - docs/ci.md
39
+ - docs/comparing.md
40
+ - docs/custom-rules.md
41
+ - docs/docker.md
42
+ - docs/findings.md
43
+ - docs/installation.md
44
+ - docs/maintainer-setup.md
45
+ - docs/plugins.md
46
+ - docs/policy.md
47
+ - docs/privacy.md
48
+ - docs/quick-start.md
49
+ - docs/rails.md
50
+ - docs/snapshot-schema.md
51
+ - docs/troubleshooting.md
52
+ - exe/bootprint
53
+ - lib/bootprint.rb
54
+ - lib/bootprint/analysis.rb
55
+ - lib/bootprint/cli.rb
56
+ - lib/bootprint/collectors/environment.rb
57
+ - lib/bootprint/collectors/filesystem.rb
58
+ - lib/bootprint/collectors/gems.rb
59
+ - lib/bootprint/collectors/libraries.rb
60
+ - lib/bootprint/collectors/operating_system.rb
61
+ - lib/bootprint/collectors/rails.rb
62
+ - lib/bootprint/collectors/runtime.rb
63
+ - lib/bootprint/collectors/toolchain.rb
64
+ - lib/bootprint/configuration.rb
65
+ - lib/bootprint/diagnosis.rb
66
+ - lib/bootprint/diff.rb
67
+ - lib/bootprint/docker.rb
68
+ - lib/bootprint/doctor.rb
69
+ - lib/bootprint/errors.rb
70
+ - lib/bootprint/formatters.rb
71
+ - lib/bootprint/formatters/human.rb
72
+ - lib/bootprint/formatters/json.rb
73
+ - lib/bootprint/formatters/markdown.rb
74
+ - lib/bootprint/formatters/sarif.rb
75
+ - lib/bootprint/initializer_profiler.rb
76
+ - lib/bootprint/plugins.rb
77
+ - lib/bootprint/policy.rb
78
+ - lib/bootprint/rails_state.rb
79
+ - lib/bootprint/railtie.rb
80
+ - lib/bootprint/rules.rb
81
+ - lib/bootprint/rules/builtin.rb
82
+ - lib/bootprint/rules/finding.rb
83
+ - lib/bootprint/rules/registry.rb
84
+ - lib/bootprint/rules/rule.rb
85
+ - lib/bootprint/sanitizer.rb
86
+ - lib/bootprint/schema.rb
87
+ - lib/bootprint/security/auditor.rb
88
+ - lib/bootprint/snapshot.rb
89
+ - lib/bootprint/version.rb
90
+ - lib/tasks/bootprint.rake
91
+ homepage: https://github.com/theworker02/bootprint
92
+ licenses:
93
+ - MIT
94
+ metadata:
95
+ source_code_uri: https://github.com/theworker02/bootprint/tree/main
96
+ homepage_uri: https://github.com/theworker02/bootprint
97
+ documentation_uri: https://github.com/theworker02/bootprint/tree/main/docs
98
+ changelog_uri: https://github.com/theworker02/bootprint/blob/main/CHANGELOG.md
99
+ bug_tracker_uri: https://github.com/theworker02/bootprint/issues
100
+ funding_uri: https://github.com/sponsors/theworker02
101
+ allowed_push_host: https://rubygems.org
102
+ rubygems_mfa_required: 'true'
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '3.1'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: 3.3.0
116
+ requirements: []
117
+ rubygems_version: 3.6.9
118
+ specification_version: 4
119
+ summary: Diagnose runtime-environment drift in Ruby applications
120
+ test_files: []