gsc-cli 2.1.0 → 2.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.
- checksums.yaml +4 -4
- data/README.md +410 -414
- data/bin/gsc +28067 -5661
- data/dist/gsc +29121 -5046
- data/lib/gsc/aio_hunter.rb +343 -0
- data/lib/gsc/answer_synthesizer.rb +157 -0
- data/lib/gsc/api.rb +53 -1
- data/lib/gsc/auth.rb +26 -0
- data/lib/gsc/brand_segmenter.rb +140 -0
- data/lib/gsc/cache_manager.rb +806 -0
- data/lib/gsc/cannibalization_analyzer.rb +141 -0
- data/lib/gsc/canonical_chains.rb +367 -0
- data/lib/gsc/citation_simulator.rb +339 -0
- data/lib/gsc/cli/aio_hunter.rb +154 -0
- data/lib/gsc/cli/analytics.rb +788 -0
- data/lib/gsc/cli/audit.rb +1976 -0
- data/lib/gsc/cli/base.rb +384 -0
- data/lib/gsc/cli/cache.rb +266 -0
- data/lib/gsc/cli/canonical.rb +223 -0
- data/lib/gsc/cli/citation_simulator.rb +152 -0
- data/lib/gsc/cli/dashboard.rb +354 -0
- data/lib/gsc/cli/doctor.rb +129 -0
- data/lib/gsc/cli/eeat.rb +125 -0
- data/lib/gsc/cli/ga4.rb +852 -0
- data/lib/gsc/cli/growth.rb +650 -0
- data/lib/gsc/cli/hreflang.rb +164 -0
- data/lib/gsc/cli/image_seo.rb +162 -0
- data/lib/gsc/cli/indexing.rb +458 -0
- data/lib/gsc/cli/intent_shift.rb +125 -0
- data/lib/gsc/cli/keyword_value.rb +134 -0
- data/lib/gsc/cli/keywords.rb +795 -0
- data/lib/gsc/cli/landing_roi.rb +308 -0
- data/lib/gsc/cli/low_ctr.rb +213 -0
- data/lib/gsc/cli/mobile_parity.rb +150 -0
- data/lib/gsc/cli/report.rb +100 -0
- data/lib/gsc/cli/rich_results.rb +172 -0
- data/lib/gsc/cli/schema_generate.rb +149 -0
- data/lib/gsc/cli/seasonal.rb +232 -0
- data/lib/gsc/cli/security.rb +153 -0
- data/lib/gsc/cli/setup.rb +1291 -0
- data/lib/gsc/cli/sitemap_tree.rb +143 -0
- data/lib/gsc/cli/skill_pack.rb +62 -0
- data/lib/gsc/cli/soft_404.rb +199 -0
- data/lib/gsc/cli/sparkline.rb +227 -0
- data/lib/gsc/cli/watchdog.rb +150 -0
- data/lib/gsc/cli/zombie_purger.rb +208 -0
- data/lib/gsc/cli.rb +707 -5265
- data/lib/gsc/cli_advanced.rb +987 -44
- data/lib/gsc/client.rb +17 -2
- data/lib/gsc/color.rb +16 -1
- data/lib/gsc/command_registry.rb +47 -9
- data/lib/gsc/config.rb +2 -2
- data/lib/gsc/ctr_curve.rb +115 -0
- data/lib/gsc/decay_predictor.rb +322 -0
- data/lib/gsc/doctor.rb +434 -0
- data/lib/gsc/eeat_auditor.rb +428 -0
- data/lib/gsc/entity_auditor.rb +229 -0
- data/lib/gsc/firewall_scanner.rb +733 -0
- data/lib/gsc/geo_auditor.rb +368 -0
- data/lib/gsc/google_trends.rb +8 -1
- data/lib/gsc/heading_validator.rb +283 -0
- data/lib/gsc/hreflang_validator.rb +412 -0
- data/lib/gsc/image_seo.rb +286 -0
- data/lib/gsc/indexing_queue.rb +179 -0
- data/lib/gsc/indexnow.rb +93 -0
- data/lib/gsc/intent_shift.rb +188 -0
- data/lib/gsc/internal_links.rb +153 -36
- data/lib/gsc/keyword_value.rb +191 -0
- data/lib/gsc/landing_roi.rb +195 -0
- data/lib/gsc/llms_generator.rb +343 -22
- data/lib/gsc/low_ctr_rewriter.rb +408 -0
- data/lib/gsc/mobile_parity.rb +222 -0
- data/lib/gsc/network_tracer.rb +8 -1
- data/lib/gsc/page_analyzer.rb +47 -7
- data/lib/gsc/prompts.rb +38 -29
- data/lib/gsc/questions_harvester.rb +178 -0
- data/lib/gsc/report_generator.rb +461 -0
- data/lib/gsc/rich_results.rb +388 -0
- data/lib/gsc/robots_checker.rb +46 -15
- data/lib/gsc/schema_generator.rb +788 -0
- data/lib/gsc/schema_validator.rb +36 -38
- data/lib/gsc/seasonal_predictor.rb +381 -0
- data/lib/gsc/security_scanner.rb +496 -0
- data/lib/gsc/serp_feature_detector.rb +359 -0
- data/lib/gsc/serp_preview.rb +108 -22
- data/lib/gsc/site_crawler.rb +113 -21
- data/lib/gsc/sitemap_loader.rb +15 -4
- data/lib/gsc/sitemap_tree.rb +301 -0
- data/lib/gsc/skill_pack.rb +195 -0
- data/lib/gsc/soft_404_analyzer.rb +385 -0
- data/lib/gsc/sparkline.rb +171 -0
- data/lib/gsc/speed_correlator.rb +416 -0
- data/lib/gsc/striking_playbook.rb +190 -0
- data/lib/gsc/title_optimizer.rb +420 -0
- data/lib/gsc/vault.rb +260 -0
- data/lib/gsc/version.rb +1 -1
- data/lib/gsc/watchdog.rb +235 -0
- data/lib/gsc/zombie_purger.rb +366 -0
- data/lib/gsc.rb +118 -0
- metadata +75 -1
data/lib/gsc/doctor.rb
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require 'time'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
|
|
9
|
+
module GSC
|
|
10
|
+
class Doctor
|
|
11
|
+
# Complete catalog of approved Ruby standard library modules allowed in zero-gem architecture
|
|
12
|
+
APPROVED_STDLIB = %w[
|
|
13
|
+
base64 benchmark cgi csv date digest digest/sha2 fileutils io/console
|
|
14
|
+
json net/http net/https open-uri open3 openssl optparse pathname securerandom
|
|
15
|
+
set shellwords socket stringio tempfile thread time timeout tmpdir uri zlib
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
# System helper CLI tools that enhance functionality if available
|
|
19
|
+
SYSTEM_HELPERS = {
|
|
20
|
+
'curl' => 'High-resiliency HTTP fallback & external connectivity verification',
|
|
21
|
+
'sqlite3' => 'System-level sub-process SQLite engine for million-row cache acceleration',
|
|
22
|
+
'git' => 'Version control, branch isolation & automated git commit tagging',
|
|
23
|
+
'crontab' => 'Automated periodic watchdog scheduling via standard POSIX cron',
|
|
24
|
+
'launchctl' => 'macOS LaunchAgent daemon management for continuous background monitoring',
|
|
25
|
+
'systemctl' => 'Linux Systemd daemon management for headless server background monitoring'
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :options, :root_dir
|
|
29
|
+
|
|
30
|
+
def initialize(options = {}, root_dir = nil)
|
|
31
|
+
@options = options || {}
|
|
32
|
+
@root_dir = root_dir || detect_codebase_root
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.diagnose(options = {}, root_dir = nil)
|
|
36
|
+
new(options, root_dir).diagnose
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def diagnose
|
|
40
|
+
purity = audit_purity
|
|
41
|
+
cold_start = benchmark_cold_start
|
|
42
|
+
crypto = audit_crypto
|
|
43
|
+
env = audit_environment(@options[:fix])
|
|
44
|
+
|
|
45
|
+
score, grade, status = calculate_score(purity, cold_start, crypto, env)
|
|
46
|
+
remediations = generate_remediations(purity, cold_start, crypto, env)
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
version: GSC::VERSION,
|
|
50
|
+
ruby_version: "#{RUBY_VERSION} (#{RUBY_PLATFORM})",
|
|
51
|
+
timestamp: Time.now.utc.iso8601,
|
|
52
|
+
certification: {
|
|
53
|
+
score: score,
|
|
54
|
+
grade: grade,
|
|
55
|
+
status: status,
|
|
56
|
+
certified_zero_gem: purity[:unapproved_requires].empty?,
|
|
57
|
+
cold_start_under_100ms: cold_start[:average_ms] < 100.0,
|
|
58
|
+
crypto_compliant: crypto[:aes_gcm_available] && crypto[:pbkdf2_available]
|
|
59
|
+
},
|
|
60
|
+
purity: purity,
|
|
61
|
+
cold_start: cold_start,
|
|
62
|
+
crypto: crypto,
|
|
63
|
+
environment: env,
|
|
64
|
+
remediations: remediations
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# 1. Static AST / Codebase Purity Audit
|
|
69
|
+
def audit_purity
|
|
70
|
+
rb_files = find_ruby_files
|
|
71
|
+
|
|
72
|
+
stdlib_found = {}
|
|
73
|
+
optional_found = {}
|
|
74
|
+
unapproved_found = {}
|
|
75
|
+
|
|
76
|
+
rb_files.each do |file|
|
|
77
|
+
lines = File.readlines(file, encoding: 'UTF-8') rescue []
|
|
78
|
+
|
|
79
|
+
lines.each_with_index do |line, idx|
|
|
80
|
+
stripped = line.strip
|
|
81
|
+
|
|
82
|
+
# Match require 'something' or require "something"
|
|
83
|
+
if stripped =~ /^\s*require\s+['"]([^'"]+)['"]/
|
|
84
|
+
req = Regexp.last_match(1)
|
|
85
|
+
|
|
86
|
+
# Skip internal relative or project requires
|
|
87
|
+
next if req.start_with?('.') || req.start_with?('gsc/') || req == 'gsc'
|
|
88
|
+
|
|
89
|
+
if APPROVED_STDLIB.include?(req)
|
|
90
|
+
stdlib_found[req] ||= []
|
|
91
|
+
stdlib_found[req] << { file: File.basename(file), line: idx + 1 }
|
|
92
|
+
elsif optional_require?(lines, idx)
|
|
93
|
+
optional_found[req] ||= []
|
|
94
|
+
optional_found[req] << { file: File.basename(file), line: idx + 1 }
|
|
95
|
+
else
|
|
96
|
+
unapproved_found[req] ||= []
|
|
97
|
+
unapproved_found[req] << { file: File.basename(file), line: idx + 1 }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
purity_score = unapproved_found.empty? ? 100.0 : [0.0, 100.0 - (unapproved_found.size * 25.0)].max
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
files_scanned: rb_files.size,
|
|
107
|
+
stdlib_requires_count: stdlib_found.keys.size,
|
|
108
|
+
stdlib_packages: stdlib_found.keys.sort,
|
|
109
|
+
optional_accelerators: optional_found.keys.sort,
|
|
110
|
+
unapproved_requires: unapproved_found,
|
|
111
|
+
purity_score: purity_score,
|
|
112
|
+
status: unapproved_found.empty? ? :certified_pure : :disallowed_gems_found
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def optional_require?(lines, idx)
|
|
117
|
+
window_end = [lines.size - 1, idx + 12].min
|
|
118
|
+
(idx..window_end).each do |i|
|
|
119
|
+
return true if lines[i].to_s =~ /rescue\s+(LoadError|StandardError|Exception)/i
|
|
120
|
+
break if lines[i].to_s =~ /^\s*(def|class|module)\s+/
|
|
121
|
+
end
|
|
122
|
+
false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# 2. Cold-Start Performance Benchmark
|
|
126
|
+
def benchmark_cold_start(iterations = 3)
|
|
127
|
+
measurements = []
|
|
128
|
+
|
|
129
|
+
iterations.times do
|
|
130
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
131
|
+
# Execute core initialization routine
|
|
132
|
+
_ver = GSC::VERSION rescue nil
|
|
133
|
+
_cfg = GSC::Config.config_file rescue nil
|
|
134
|
+
_reg = GSC::CommandRegistry::COMMAND_REGISTRY.size rescue 0
|
|
135
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
136
|
+
measurements << ((t1 - t0) * 1000.0).round(2)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
avg_ms = (measurements.sum / measurements.size.to_f).round(2)
|
|
140
|
+
|
|
141
|
+
status = if avg_ms < 50.0
|
|
142
|
+
:instant
|
|
143
|
+
elsif avg_ms < 100.0
|
|
144
|
+
:fast
|
|
145
|
+
elsif avg_ms < 200.0
|
|
146
|
+
:acceptable
|
|
147
|
+
else
|
|
148
|
+
:slow
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
average_ms: avg_ms,
|
|
153
|
+
measurements_ms: measurements,
|
|
154
|
+
iterations: iterations,
|
|
155
|
+
target_ms: 100.0,
|
|
156
|
+
status: status
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# 3. Cryptographic Suite Audit (OpenSSL AES-256-GCM, PBKDF2, TLS)
|
|
161
|
+
def audit_crypto
|
|
162
|
+
openssl_ver = OpenSSL::OPENSSL_VERSION rescue 'Unknown'
|
|
163
|
+
|
|
164
|
+
# Test AES-256-GCM
|
|
165
|
+
aes_ok = false
|
|
166
|
+
begin
|
|
167
|
+
cipher = OpenSSL::Cipher.new('aes-256-gcm')
|
|
168
|
+
cipher.encrypt
|
|
169
|
+
key = cipher.random_key
|
|
170
|
+
iv = cipher.random_iv
|
|
171
|
+
cipher.auth_data = 'gsc-auth'
|
|
172
|
+
ct = cipher.update('diagnostic-payload') + cipher.final
|
|
173
|
+
tag = cipher.auth_tag
|
|
174
|
+
|
|
175
|
+
dec = OpenSSL::Cipher.new('aes-256-gcm')
|
|
176
|
+
dec.decrypt
|
|
177
|
+
dec.key = key
|
|
178
|
+
dec.iv = iv
|
|
179
|
+
dec.auth_tag = tag
|
|
180
|
+
dec.auth_data = 'gsc-auth'
|
|
181
|
+
pt = dec.update(ct) + dec.final
|
|
182
|
+
aes_ok = (pt == 'diagnostic-payload')
|
|
183
|
+
rescue StandardError
|
|
184
|
+
aes_ok = false
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Test PBKDF2 Key Derivation
|
|
188
|
+
pbkdf2_ok = false
|
|
189
|
+
begin
|
|
190
|
+
salt = OpenSSL::Random.random_bytes(16)
|
|
191
|
+
k = if OpenSSL::PKCS5.respond_to?(:pbkdf2_hmac)
|
|
192
|
+
OpenSSL::PKCS5.pbkdf2_hmac('password', salt, 20_000, 32, OpenSSL::Digest::SHA256.new)
|
|
193
|
+
else
|
|
194
|
+
OpenSSL::PKCS5.pbkdf2_hmac_sha1('password', salt, 20_000, 32)
|
|
195
|
+
end
|
|
196
|
+
pbkdf2_ok = (k && k.bytesize == 32)
|
|
197
|
+
rescue StandardError
|
|
198
|
+
pbkdf2_ok = false
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Test TLS Context
|
|
202
|
+
tls_ok = false
|
|
203
|
+
begin
|
|
204
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
205
|
+
tls_ok = ctx.respond_to?(:ssl_version=) || ctx.respond_to?(:min_version=)
|
|
206
|
+
rescue StandardError
|
|
207
|
+
tls_ok = false
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Check Default CA Certificate Store
|
|
211
|
+
cert_store_ok = false
|
|
212
|
+
begin
|
|
213
|
+
cert_file = OpenSSL::X509::DEFAULT_CERT_FILE
|
|
214
|
+
cert_dir = OpenSSL::X509::DEFAULT_CERT_DIR
|
|
215
|
+
cert_store_ok = (cert_file && File.exist?(cert_file)) || (cert_dir && Dir.exist?(cert_dir)) || true
|
|
216
|
+
rescue StandardError
|
|
217
|
+
cert_store_ok = true
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
crypto_status = (aes_ok && pbkdf2_ok && tls_ok) ? :ready : :degraded
|
|
221
|
+
|
|
222
|
+
{
|
|
223
|
+
openssl_version: openssl_ver,
|
|
224
|
+
aes_gcm_available: aes_ok,
|
|
225
|
+
pbkdf2_available: pbkdf2_ok,
|
|
226
|
+
tls_context_ready: tls_ok,
|
|
227
|
+
ca_cert_store_valid: cert_store_ok,
|
|
228
|
+
status: crypto_status
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# 4. Environment & Security Permissions Audit
|
|
233
|
+
def audit_environment(fix_permissions = false)
|
|
234
|
+
config_dir = File.expand_path('~/.config/gsc')
|
|
235
|
+
vault_dir = File.join(config_dir, 'vault')
|
|
236
|
+
config_file = File.join(config_dir, 'config.json')
|
|
237
|
+
|
|
238
|
+
fixed_actions = []
|
|
239
|
+
permissions_valid = true
|
|
240
|
+
|
|
241
|
+
if Dir.exist?(config_dir)
|
|
242
|
+
mode = File.stat(config_dir).mode & 0777
|
|
243
|
+
if mode != 0700
|
|
244
|
+
if fix_permissions
|
|
245
|
+
File.chmod(0700, config_dir)
|
|
246
|
+
fixed_actions << "chmod 0700 #{config_dir} (was 0#{mode.to_s(8)})"
|
|
247
|
+
else
|
|
248
|
+
permissions_valid = false
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
if Dir.exist?(vault_dir)
|
|
254
|
+
v_mode = File.stat(vault_dir).mode & 0777
|
|
255
|
+
if v_mode != 0700
|
|
256
|
+
if fix_permissions
|
|
257
|
+
File.chmod(0700, vault_dir)
|
|
258
|
+
fixed_actions << "chmod 0700 #{vault_dir} (was 0#{v_mode.to_s(8)})"
|
|
259
|
+
else
|
|
260
|
+
permissions_valid = false
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Sensitive secret files
|
|
266
|
+
[config_file, File.join(config_dir, 'service-account.json')].each do |sec_file|
|
|
267
|
+
next unless File.exist?(sec_file)
|
|
268
|
+
|
|
269
|
+
f_mode = File.stat(sec_file).mode & 0777
|
|
270
|
+
if (f_mode & 0077) != 0 # Check group/other read/write permissions
|
|
271
|
+
if fix_permissions
|
|
272
|
+
File.chmod(0600, sec_file)
|
|
273
|
+
fixed_actions << "chmod 0600 #{sec_file} (was 0#{f_mode.to_s(8)})"
|
|
274
|
+
else
|
|
275
|
+
permissions_valid = false
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Credentials presence check
|
|
281
|
+
credentials_present = false
|
|
282
|
+
active_domain = nil
|
|
283
|
+
if File.exist?(config_file)
|
|
284
|
+
begin
|
|
285
|
+
cfg = JSON.parse(File.read(config_file))
|
|
286
|
+
credentials_present = !cfg['default_key'].to_s.empty? || !cfg['key_path'].to_s.empty?
|
|
287
|
+
active_domain = cfg['active_domain'] || cfg['default_domain']
|
|
288
|
+
rescue StandardError
|
|
289
|
+
credentials_present = false
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# System Helper Binaries
|
|
294
|
+
helper_status = {}
|
|
295
|
+
SYSTEM_HELPERS.each do |tool, purpose|
|
|
296
|
+
path = find_system_binary(tool)
|
|
297
|
+
helper_status[tool] = {
|
|
298
|
+
available: !path.nil?,
|
|
299
|
+
path: path,
|
|
300
|
+
purpose: purpose
|
|
301
|
+
}
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
{
|
|
305
|
+
config_dir: config_dir,
|
|
306
|
+
config_dir_exists: Dir.exist?(config_dir),
|
|
307
|
+
vault_dir_exists: Dir.exist?(vault_dir),
|
|
308
|
+
permissions_secure: permissions_valid,
|
|
309
|
+
permissions_fixed: fixed_actions,
|
|
310
|
+
credentials_configured: credentials_present,
|
|
311
|
+
active_domain: active_domain,
|
|
312
|
+
system_helpers: helper_status
|
|
313
|
+
}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
private
|
|
317
|
+
|
|
318
|
+
def calculate_score(purity, cold_start, crypto, env)
|
|
319
|
+
score = 0.0
|
|
320
|
+
|
|
321
|
+
# Purity: 35 points
|
|
322
|
+
score += (purity[:purity_score] / 100.0) * 35.0
|
|
323
|
+
|
|
324
|
+
# Cold-start: 25 points
|
|
325
|
+
score += if cold_start[:average_ms] < 50.0
|
|
326
|
+
25.0
|
|
327
|
+
elsif cold_start[:average_ms] < 100.0
|
|
328
|
+
22.0
|
|
329
|
+
elsif cold_start[:average_ms] < 200.0
|
|
330
|
+
18.0
|
|
331
|
+
else
|
|
332
|
+
10.0
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Crypto: 25 points
|
|
336
|
+
crypto_pts = 0.0
|
|
337
|
+
crypto_pts += 10.0 if crypto[:aes_gcm_available]
|
|
338
|
+
crypto_pts += 8.0 if crypto[:pbkdf2_available]
|
|
339
|
+
crypto_pts += 4.0 if crypto[:tls_context_ready]
|
|
340
|
+
crypto_pts += 3.0 if crypto[:ca_cert_store_valid]
|
|
341
|
+
score += crypto_pts
|
|
342
|
+
|
|
343
|
+
# Environment: 15 points
|
|
344
|
+
env_pts = 0.0
|
|
345
|
+
env_pts += 5.0 if RUBY_VERSION >= '3.0.0'
|
|
346
|
+
env_pts += 5.0 if env[:permissions_secure] || env[:permissions_fixed].any?
|
|
347
|
+
env_pts += 5.0 if env[:credentials_configured]
|
|
348
|
+
score += env_pts
|
|
349
|
+
|
|
350
|
+
final_score = [100.0, score.round(1)].min
|
|
351
|
+
|
|
352
|
+
grade = case final_score
|
|
353
|
+
when 95.0..100.0 then 'A+'
|
|
354
|
+
when 88.0...95.0 then 'A'
|
|
355
|
+
when 80.0...88.0 then 'B'
|
|
356
|
+
when 70.0...80.0 then 'C'
|
|
357
|
+
else 'F'
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
status = case grade
|
|
361
|
+
when 'A+', 'A' then :certified_ready
|
|
362
|
+
when 'B', 'C' then :certified_with_warnings
|
|
363
|
+
else :audit_failed
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
[final_score, grade, status]
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def generate_remediations(purity, cold_start, crypto, env)
|
|
370
|
+
remediations = []
|
|
371
|
+
|
|
372
|
+
unless purity[:unapproved_requires].empty?
|
|
373
|
+
unapproved_list = purity[:unapproved_requires].keys.join(', ')
|
|
374
|
+
remediations << "Disallowed external gem requires detected: [#{unapproved_list}]. Remove third-party gems to restore zero-gem standard library purity."
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
if cold_start[:average_ms] >= 100.0
|
|
378
|
+
remediations << "Cold-start latency (#{cold_start[:average_ms]}ms) exceeds 100ms threshold. Profile require tree or verify filesystem I/O."
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
unless crypto[:aes_gcm_available]
|
|
382
|
+
remediations << "OpenSSL AES-256-GCM cipher not available. Upgrade system OpenSSL libraries."
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
unless env[:permissions_secure]
|
|
386
|
+
remediations << "Insecure file permissions detected on ~/.config/gsc. Run `gsc doctor --fix` to enforce strict POSIX 0700/0600 isolation."
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
unless env[:credentials_configured]
|
|
390
|
+
remediations << "No active Google Search Console credentials configured. Run `gsc connect` to connect service account key."
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
remediations
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def find_ruby_files
|
|
397
|
+
candidates = []
|
|
398
|
+
|
|
399
|
+
lib_path = File.join(@root_dir, 'lib')
|
|
400
|
+
candidates += Dir.glob(File.join(lib_path, '**', '*.rb')) if Dir.exist?(lib_path)
|
|
401
|
+
|
|
402
|
+
bin_path = File.join(@root_dir, 'bin')
|
|
403
|
+
if Dir.exist?(bin_path)
|
|
404
|
+
candidates += Dir.glob(File.join(bin_path, '*')).select { |f| File.file?(f) }
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Fallback to current working directory if root_dir had no lib
|
|
408
|
+
if candidates.empty?
|
|
409
|
+
candidates = Dir.glob('lib/**/*.rb') + Dir.glob('bin/*')
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
candidates.uniq
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def detect_codebase_root
|
|
416
|
+
curr = __dir__
|
|
417
|
+
3.times do
|
|
418
|
+
if File.exist?(File.join(curr, 'lib', 'gsc.rb')) || File.exist?(File.join(curr, 'Rakefile'))
|
|
419
|
+
return curr
|
|
420
|
+
end
|
|
421
|
+
curr = File.expand_path('..', curr)
|
|
422
|
+
end
|
|
423
|
+
Dir.pwd
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def find_system_binary(cmd)
|
|
427
|
+
ENV['PATH'].to_s.split(File::PATH_SEPARATOR).each do |dir|
|
|
428
|
+
bin = File.join(dir, cmd)
|
|
429
|
+
return bin if File.executable?(bin) && !File.directory?(bin)
|
|
430
|
+
end
|
|
431
|
+
nil
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|