keysloth 0.1.1

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.
@@ -0,0 +1,468 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module KeySloth
6
+ # Интерфейс командной строки для KeySloth
7
+ #
8
+ # Предоставляет команды pull, push и help для управления секретами.
9
+ # Использует Thor для парсинга аргументов командной строки.
10
+ #
11
+ # @example Использование CLI
12
+ # # Получение секретов
13
+ # keysloth pull --repo git@github.com:company/secrets.git --password secret
14
+ #
15
+ # # Отправка секретов
16
+ # keysloth push --repo git@github.com:company/secrets.git --password secret
17
+ #
18
+ # # Справка
19
+ # keysloth help
20
+ #
21
+ # @author KeySloth Team
22
+ # @since 0.1.0
23
+ class CLI < Thor
24
+ class_option :verbose, type: :boolean, aliases: '-v',
25
+ desc: 'Включить подробное логирование (DEBUG уровень)'
26
+ class_option :quiet, type: :boolean, aliases: '-q',
27
+ desc: 'Тихий режим (только ошибки)'
28
+ class_option :config, type: :string, aliases: '-c',
29
+ desc: 'Путь к файлу конфигурации .keyslothrc'
30
+
31
+ desc 'pull', 'Получить и расшифровать секреты из Git репозитория'
32
+ long_desc <<~DESC
33
+ Команда pull клонирует указанный Git репозиторий, получает зашифрованные файлы
34
+ из указанной ветки, расшифровывает их с использованием предоставленного пароля
35
+ и сохраняет в локальную директорию.
36
+
37
+ Поддерживаемые типы файлов секретов:
38
+ - .cer (сертификаты)
39
+ - .p12 (PKCS#12 сертификаты)
40
+ - .mobileprovisioning (профили подготовки iOS)
41
+ - .json (конфигурационные файлы)
42
+
43
+ Перед выполнением операции автоматически создается резервная копия
44
+ существующей локальной директории с секретами.
45
+ DESC
46
+ option :repo, type: :string, aliases: '-r',
47
+ desc: 'URL Git репозитория (SSH: git@github.com:user/repo.git)'
48
+ option :branch, type: :string, default: 'main', aliases: '-b',
49
+ desc: 'Ветка репозитория для получения секретов'
50
+ option :password, type: :string, required: true, aliases: '-p',
51
+ desc: 'Пароль для расшифровки секретов'
52
+ option :path, type: :string, default: './secrets', aliases: '-d',
53
+ desc: 'Локальный путь для сохранения расшифрованных секретов'
54
+ def pull
55
+ setup_logger
56
+
57
+ logger.info('=== Начинаем операцию получения секретов ===')
58
+
59
+ begin
60
+ KeySloth.pull(
61
+ repo_url: options[:repo],
62
+ branch: options[:branch],
63
+ password: options[:password],
64
+ local_path: options[:path],
65
+ config_file: options[:config]
66
+ )
67
+
68
+ logger.info('=== Операция получения секретов завершена успешно ===')
69
+ rescue KeySloth::KeySlothError => e
70
+ logger.error("Ошибка выполнения операции: #{e.message}")
71
+ exit 1
72
+ end
73
+ end
74
+
75
+ desc 'push', 'Зашифровать и отправить секреты в Git репозиторий'
76
+ long_desc <<~DESC
77
+ Команда push читает файлы секретов из локальной директории, шифрует их
78
+ с использованием предоставленного пароля и отправляет в указанный
79
+ Git репозиторий в указанную ветку.
80
+
81
+ Перед отправкой выполняется проверка актуальности удаленной ветки.
82
+ При обнаружении конфликтов операция прекращается с детальным сообщением
83
+ об ошибке.
84
+
85
+ Поддерживаемые типы файлов секретов:
86
+ - .cer (сертификаты)
87
+ - .p12 (PKCS#12 сертификаты)#{' '}
88
+ - .mobileprovisioning (профили подготовки iOS)
89
+ - .json (конфигурационные файлы)
90
+ DESC
91
+ option :repo, type: :string, aliases: '-r',
92
+ desc: 'URL Git репозитория (SSH: git@github.com:user/repo.git)'
93
+ option :branch, type: :string, default: 'main', aliases: '-b',
94
+ desc: 'Ветка репозитория для отправки секретов'
95
+ option :password, type: :string, required: true, aliases: '-p',
96
+ desc: 'Пароль для шифрования секретов'
97
+ option :path, type: :string, default: './secrets', aliases: '-d',
98
+ desc: 'Локальный путь с секретами для шифрования'
99
+ option :message, type: :string, aliases: '-m',
100
+ desc: 'Сообщение коммита (опционально)'
101
+ def push
102
+ setup_logger
103
+
104
+ logger.info('=== Начинаем операцию отправки секретов ===')
105
+
106
+ begin
107
+ KeySloth.push(
108
+ repo_url: options[:repo],
109
+ branch: options[:branch],
110
+ password: options[:password],
111
+ local_path: options[:path],
112
+ config_file: options[:config],
113
+ commit_message: options[:message]
114
+ )
115
+
116
+ logger.info('=== Операция отправки секретов завершена успешно ===')
117
+ rescue KeySloth::KeySlothError => e
118
+ logger.error("Ошибка выполнения операции: #{e.message}")
119
+ exit 1
120
+ end
121
+ end
122
+
123
+ desc 'status', 'Проверить состояние локальных секретов'
124
+ long_desc <<~DESC
125
+ Команда status показывает информацию о состоянии локальных секретов:
126
+ - Количество найденных файлов секретов
127
+ - Список файлов с их размерами
128
+ - Информацию о доступных резервных копиях
129
+ - Проверку целостности файлов
130
+ DESC
131
+ option :path, type: :string, default: './secrets', aliases: '-d',
132
+ desc: 'Локальный путь с секретами для проверки'
133
+ def status
134
+ setup_logger
135
+
136
+ logger.info('=== Проверяем состояние локальных секретов ===')
137
+
138
+ begin
139
+ file_manager = FileManager.new(logger)
140
+
141
+ unless file_manager.directory_exists?(options[:path])
142
+ logger.warn("Директория секретов не существует: #{options[:path]}")
143
+ return
144
+ end
145
+
146
+ # Собираем файлы секретов
147
+ secret_files = file_manager.collect_secret_files(options[:path])
148
+
149
+ if secret_files.empty?
150
+ logger.info('Файлы секретов не найдены')
151
+ return
152
+ end
153
+
154
+ logger.info("Найдено #{secret_files.size} файлов секретов:")
155
+
156
+ secret_files.each do |file_path|
157
+ size = File.size(file_path)
158
+ relative_path = file_manager.get_relative_path(file_path, options[:path])
159
+ integrity = file_manager.verify_file_integrity(file_path) ? '✓' : '✗'
160
+
161
+ logger.info(" #{integrity} #{relative_path} (#{size} байт)")
162
+ end
163
+
164
+ # Показываем информацию о backup'ах
165
+ backups = file_manager.list_backups(options[:path])
166
+ if backups.any?
167
+ logger.info("\nДоступные резервные копии:")
168
+ backups.each do |backup_path|
169
+ backup_time = File.basename(backup_path).match(/_(\d{8}_\d{6})$/)&.captures&.first
170
+ if backup_time
171
+ formatted_time = Time.strptime(backup_time,
172
+ '%Y%m%d_%H%M%S').strftime('%Y-%m-%d %H:%M:%S')
173
+ logger.info(" #{File.basename(backup_path)} (#{formatted_time})")
174
+ else
175
+ logger.info(" #{File.basename(backup_path)}")
176
+ end
177
+ end
178
+ else
179
+ logger.info("\nРезервные копии не найдены")
180
+ end
181
+ rescue KeySloth::KeySlothError => e
182
+ logger.error("Ошибка проверки состояния: #{e.message}")
183
+ exit 1
184
+ end
185
+ end
186
+
187
+ desc 'restore BACKUP_PATH', 'Восстановить секреты из резервной копии'
188
+ long_desc <<~DESC
189
+ Команда restore восстанавливает локальные секреты из указанной
190
+ резервной копии. Используйте команду 'status' для просмотра
191
+ доступных резервных копий.
192
+ DESC
193
+ option :path, type: :string, default: './secrets', aliases: '-d',
194
+ desc: 'Локальный путь для восстановления секретов'
195
+ def restore(backup_path)
196
+ setup_logger
197
+
198
+ logger.info('=== Восстанавливаем секреты из резервной копии ===')
199
+
200
+ begin
201
+ file_manager = FileManager.new(logger)
202
+ file_manager.restore_from_backup(backup_path, options[:path])
203
+
204
+ logger.info('=== Восстановление завершено успешно ===')
205
+ rescue KeySloth::KeySlothError => e
206
+ logger.error("Ошибка восстановления: #{e.message}")
207
+ exit 1
208
+ end
209
+ end
210
+
211
+ desc 'validate', 'Проверить целостность файлов секретов'
212
+ long_desc <<~DESC
213
+ Команда validate выполняет проверку целостности всех файлов секретов
214
+ в указанной директории:
215
+ - Проверяет доступность файлов для чтения
216
+ - Проверяет что файлы не пустые
217
+ - Валидирует структуру файлов секретов
218
+ - Выводит детальный отчет о состоянии каждого файла
219
+ DESC
220
+ option :path, type: :string, default: './secrets', aliases: '-d',
221
+ desc: 'Локальный путь с секретами для проверки'
222
+ def validate
223
+ setup_logger
224
+
225
+ logger.info('=== Проверяем целостность файлов секретов ===')
226
+
227
+ begin
228
+ file_manager = FileManager.new(logger)
229
+
230
+ unless file_manager.directory_exists?(options[:path])
231
+ logger.error("Директория секретов не существует: #{options[:path]}")
232
+ exit 1
233
+ end
234
+
235
+ # Собираем файлы секретов
236
+ secret_files = file_manager.collect_secret_files(options[:path])
237
+
238
+ if secret_files.empty?
239
+ logger.warn('Файлы секретов не найдены')
240
+ return
241
+ end
242
+
243
+ logger.info("Проверяем #{secret_files.size} файлов секретов...")
244
+
245
+ valid_files = 0
246
+ invalid_files = 0
247
+
248
+ secret_files.each do |file_path|
249
+ relative_path = file_manager.get_relative_path(file_path, options[:path])
250
+
251
+ if file_manager.verify_file_integrity(file_path)
252
+ logger.info(" ✓ #{relative_path} - файл корректен")
253
+ valid_files += 1
254
+ else
255
+ logger.error(" ✗ #{relative_path} - файл поврежден или недоступен")
256
+ invalid_files += 1
257
+ end
258
+ end
259
+
260
+ # Итоговый отчет
261
+ logger.info("\n=== Результаты проверки ===")
262
+ logger.info("Корректных файлов: #{valid_files}")
263
+ logger.info("Поврежденных файлов: #{invalid_files}")
264
+
265
+ if invalid_files.positive?
266
+ logger.error("Обнаружены поврежденные файлы! Рекомендуется восстановление из backup'а.")
267
+ exit 1
268
+ else
269
+ logger.info('Все файлы секретов прошли проверку целостности.')
270
+ end
271
+ rescue KeySloth::KeySlothError => e
272
+ logger.error("Ошибка проверки целостности: #{e.message}")
273
+ exit 1
274
+ end
275
+ end
276
+
277
+ desc 'init', 'Инициализировать новый проект с KeySloth'
278
+ long_desc <<~DESC
279
+ Команда init выполняет первичную настройку KeySloth в проекте:
280
+ - Создает файл конфигурации .keyslothrc
281
+ - Создает локальную директорию для секретов
282
+ - Добавляет директорию секретов в .gitignore
283
+ - Создает примеры конфигурации
284
+
285
+ После выполнения команды можно использовать pull и push для работы с секретами.
286
+ DESC
287
+ option :repo, type: :string, required: true, aliases: '-r',
288
+ desc: 'URL Git репозитория для хранения секретов'
289
+ option :branch, type: :string, default: 'main', aliases: '-b',
290
+ desc: 'Ветка репозитория для секретов'
291
+ option :path, type: :string, default: './secrets', aliases: '-d',
292
+ desc: 'Локальный путь для секретов'
293
+ option :force, type: :boolean, aliases: '-f',
294
+ desc: 'Перезаписать существующие файлы конфигурации'
295
+ def init
296
+ setup_logger
297
+
298
+ logger.info('=== Инициализируем KeySloth проект ===')
299
+
300
+ begin
301
+ file_manager = FileManager.new(logger)
302
+
303
+ config_path = '.keyslothrc'
304
+ gitignore_path = '.gitignore'
305
+ secrets_path = options[:path]
306
+
307
+ # Проверяем существование конфигурации
308
+ if File.exist?(config_path) && !options[:force]
309
+ logger.error("Файл #{config_path} уже существует. Используйте --force для перезаписи.")
310
+ exit 1
311
+ end
312
+
313
+ # Создаем конфигурационный файл
314
+ config_content = <<~YAML
315
+ # Конфигурация KeySloth
316
+ repo_url: "#{options[:repo]}"
317
+ branch: "#{options[:branch]}"
318
+ local_path: "#{secrets_path}"
319
+ backup_count: 3
320
+ YAML
321
+
322
+ File.write(config_path, config_content)
323
+ logger.info("Создан файл конфигурации: #{config_path}")
324
+
325
+ # Создаем директорию для секретов
326
+ file_manager.ensure_directory(secrets_path)
327
+ logger.info("Создана директория для секретов: #{secrets_path}")
328
+
329
+ # Обновляем .gitignore
330
+ gitignore_entry = "\n# KeySloth секреты\n#{secrets_path}/\n.keyslothrc\n"
331
+
332
+ if File.exist?(gitignore_path)
333
+ gitignore_content = File.read(gitignore_path)
334
+ if gitignore_content.include?(secrets_path)
335
+ logger.info('Файл .gitignore уже содержит правила для KeySloth')
336
+ else
337
+ File.write(gitignore_path, gitignore_content + gitignore_entry)
338
+ logger.info('Обновлен файл .gitignore')
339
+ end
340
+ else
341
+ File.write(gitignore_path, gitignore_entry.strip)
342
+ logger.info('Создан файл .gitignore')
343
+ end
344
+
345
+ # Создаем README для директории секретов
346
+ readme_path = File.join(secrets_path, 'README.md')
347
+ readme_content = <<~MARKDOWN
348
+ # Секреты проекта
349
+
350
+ Эта директория содержит зашифрованные секреты проекта, управляемые KeySloth.
351
+
352
+ ## Использование
353
+
354
+ ### Получение секретов
355
+ ```bash
356
+ keysloth pull
357
+ ```
358
+
359
+ ### Отправка секретов
360
+ ```bash
361
+ keysloth push
362
+ ```
363
+
364
+ ### Проверка состояния
365
+ ```bash
366
+ keysloth status
367
+ ```
368
+
369
+ ## Поддерживаемые типы файлов
370
+ - `.cer` - сертификаты
371
+ - `.p12` - PKCS#12 сертификаты
372
+ - `.mobileprovisioning` - профили подготовки iOS
373
+ - `.json` - конфигурационные файлы
374
+
375
+ **ВНИМАНИЕ:** Никогда не коммитьте эту директорию в Git!
376
+ MARKDOWN
377
+
378
+ File.write(readme_path, readme_content)
379
+ logger.info("Создан файл справки: #{readme_path}")
380
+
381
+ logger.info("\n=== Инициализация завершена успешно ===")
382
+ logger.info('Следующие шаги:')
383
+ logger.info("1. Поместите ваши файлы секретов в #{secrets_path}/")
384
+ logger.info("2. Используйте 'keysloth push' для первой отправки секретов")
385
+ logger.info('3. Поделитесь паролем шифрования с командой')
386
+ rescue KeySloth::KeySlothError => e
387
+ logger.error("Ошибка инициализации: #{e.message}")
388
+ exit 1
389
+ rescue StandardError => e
390
+ logger.error("Неожиданная ошибка: #{e.message}")
391
+ exit 1
392
+ end
393
+ end
394
+
395
+ desc 'version', 'Показать версию KeySloth'
396
+ def version
397
+ puts "KeySloth версия #{KeySloth::VERSION}"
398
+ end
399
+
400
+ # Переопределяем help для улучшенного вывода
401
+ desc 'help [COMMAND]', 'Показать справку по командам'
402
+ def help(command = nil)
403
+ if command
404
+ super
405
+ else
406
+ puts <<~HELP
407
+ KeySloth v#{KeySloth::VERSION} - Управление зашифрованными секретами в Git репозиториях
408
+
409
+ ИСПОЛЬЗОВАНИЕ:
410
+ keysloth COMMAND [OPTIONS]
411
+
412
+ КОМАНДЫ:
413
+ init Инициализировать новый проект с KeySloth
414
+ pull Получить и расшифровать секреты из Git репозитория
415
+ push Зашифровать и отправить секреты в Git репозиторий#{' '}
416
+ status Проверить состояние локальных секретов
417
+ validate Проверить целостность файлов секретов
418
+ restore Восстановить секреты из резервной копии
419
+ version Показать версию KeySloth
420
+ help Показать эту справку
421
+
422
+ ГЛОБАЛЬНЫЕ ОПЦИИ:
423
+ -v, --verbose Подробное логирование (DEBUG уровень)
424
+ -q, --quiet Тихий режим (только ошибки)
425
+ -c, --config Путь к файлу конфигурации .keyslothrc
426
+
427
+ ПРИМЕРЫ:
428
+ # Инициализировать новый проект
429
+ keysloth init -r git@github.com:company/secrets.git
430
+
431
+ # Получить секреты из репозитория
432
+ keysloth pull -r git@github.com:company/secrets.git -p mypassword
433
+
434
+ # Отправить секреты в репозиторий
435
+ keysloth push -r git@github.com:company/secrets.git -p mypassword -m "Update certificates"
436
+
437
+ # Проверить состояние и целостность
438
+ keysloth status
439
+ keysloth validate
440
+
441
+ Для подробной справки по команде используйте: keysloth help COMMAND
442
+ HELP
443
+ end
444
+ end
445
+
446
+ private
447
+
448
+ # Настраивает логгер в соответствии с параметрами командной строки
449
+ def setup_logger
450
+ level = if options[:verbose]
451
+ :debug
452
+ elsif options[:quiet]
453
+ :error
454
+ else
455
+ :info
456
+ end
457
+
458
+ @logger = Logger.new(level: level)
459
+ end
460
+
461
+ # Возвращает настроенный логгер
462
+ #
463
+ # @return [KeySloth::Logger] Настроенный логгер
464
+ def logger
465
+ @logger ||= Logger.new
466
+ end
467
+ end
468
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'pathname'
5
+
6
+ module KeySloth
7
+ # Класс для управления конфигурацией KeySloth
8
+ #
9
+ # Поддерживает загрузку конфигурации из YAML файла .keyslothrc.
10
+ # Параметры командной строки имеют приоритет над файлом конфигурации.
11
+ #
12
+ # @example Структура файла .keyslothrc
13
+ # repo_url: "git@github.com:company/secrets.git"
14
+ # branch: "main"
15
+ # local_path: "./secrets"
16
+ # backup_count: 3
17
+ #
18
+ # @example Использование
19
+ # config = KeySloth::Config.load('.keyslothrc')
20
+ # merged = config.merge(repo_url: 'override_url')
21
+ #
22
+ # @author KeySloth Team
23
+ # @since 0.1.0
24
+ class Config
25
+ # Значения конфигурации по умолчанию
26
+ DEFAULT_CONFIG = {
27
+ branch: 'main',
28
+ backup_count: 3,
29
+ local_path: './secrets'
30
+ }.freeze
31
+
32
+ # Инициализация объекта конфигурации
33
+ #
34
+ # @param config_hash [Hash] Хеш с параметрами конфигурации
35
+ def initialize(config_hash = {})
36
+ @config = DEFAULT_CONFIG.merge(symbolize_keys(config_hash))
37
+ end
38
+
39
+ # Загружает конфигурацию из файла
40
+ #
41
+ # @param config_file [String, nil] Путь к файлу конфигурации
42
+ # @return [Config] Объект конфигурации
43
+ # @raise [ConfigurationError] при ошибках чтения файла конфигурации
44
+ def self.load(config_file = nil)
45
+ config_path = resolve_config_path(config_file)
46
+
47
+ if config_path && File.exist?(config_path)
48
+ begin
49
+ yaml_content = YAML.load_file(config_path)
50
+ new(yaml_content || {})
51
+ rescue StandardError => e
52
+ raise ConfigurationError, "Ошибка чтения конфигурации из #{config_path}: #{e.message}", e
53
+ end
54
+ else
55
+ new
56
+ end
57
+ end
58
+
59
+ # Объединяет текущую конфигурацию с новыми параметрами
60
+ #
61
+ # @param overrides [Hash] Параметры для переопределения
62
+ # @return [Hash] Объединенная конфигурация
63
+ def merge(overrides = {})
64
+ @config.merge(symbolize_keys(overrides))
65
+ end
66
+
67
+ # Возвращает значение конфигурации по ключу
68
+ #
69
+ # @param key [Symbol, String] Ключ конфигурации
70
+ # @return [Object] Значение конфигурации
71
+ def [](key)
72
+ @config[key.to_sym]
73
+ end
74
+
75
+ # Устанавливает значение конфигурации
76
+ #
77
+ # @param key [Symbol, String] Ключ конфигурации
78
+ # @param value [Object] Значение конфигурации
79
+ def []=(key, value)
80
+ @config[key.to_sym] = value
81
+ end
82
+
83
+ # Возвращает все параметры конфигурации
84
+ #
85
+ # @return [Hash] Хеш конфигурации
86
+ def to_h
87
+ @config.dup
88
+ end
89
+
90
+ # Валидирует обязательные параметры конфигурации
91
+ #
92
+ # @param required_keys [Array<Symbol>] Список обязательных ключей
93
+ # @raise [ValidationError] при отсутствии обязательных параметров
94
+ def validate!(required_keys = [])
95
+ missing_keys = required_keys.select { |key| @config[key].nil? || @config[key].to_s.empty? }
96
+
97
+ unless missing_keys.empty?
98
+ raise ValidationError, "Отсутствуют обязательные параметры: #{missing_keys.join(', ')}"
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ # Преобразует строковые ключи в символы
105
+ #
106
+ # @param hash [Hash] Исходный хеш
107
+ # @return [Hash] Хеш с символьными ключами
108
+ def symbolize_keys(hash)
109
+ return {} unless hash.is_a?(Hash)
110
+
111
+ hash.transform_keys(&:to_sym)
112
+ end
113
+
114
+ # Определяет путь к файлу конфигурации
115
+ #
116
+ # @param config_file [String, nil] Явно указанный путь к файлу
117
+ # @return [String, nil] Путь к файлу конфигурации или nil
118
+ def self.resolve_config_path(config_file)
119
+ return config_file if config_file
120
+
121
+ # Ищем .keyslothrc в текущей директории и домашней директории
122
+ ['.keyslothrc', File.expand_path('~/.keyslothrc')].find do |path|
123
+ File.exist?(path)
124
+ end
125
+ end
126
+
127
+ private_class_method :resolve_config_path
128
+ end
129
+ end