Last active
October 2, 2021 00:48
-
-
Save RISCfuture/2e8a250b884619c44c6d686498773b9a to your computer and use it in GitHub Desktop.
Scan and fix 1Password passwords
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### 1password-scan-fix.rb ### | |
# | |
# This script locates 1Password passwords linked to URLs that either a) | |
# redirect to a newer URL or b) fail to load. For each of these URLs, you will | |
# be prompted as to whether you would like to modify or delete the URL. | |
# | |
# This script requires the | |
# [1Password CLI](https://1password.com/downloads/command-line/) | |
# (`brew install 1password-cli`). You will need to sign in to 1Password CLI | |
# by following the instructions on that web page first. | |
# | |
# You will also need the `json` and `activesupport` gems installed. | |
require 'json' | |
require 'uri' | |
require 'net/http' | |
require 'active_support' | |
require 'open3' | |
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36' | |
verbose = ARGV.include?('--verbose') | |
ARGV.delete '--verbose' | |
class URI::HTTP | |
def host_portion | |
to_s.sub(path, '') | |
end | |
end | |
def sign_in | |
result = `op signin` | |
if (matches = result.match(/export (.+?)="(.+)"/)) | |
ENV[matches[1]] = matches[2] | |
else | |
sign_in | |
end | |
end | |
def op(*args) | |
stdout, stderr, status = Open3.capture3('op', *args) | |
if stderr.include?('You are not currently signed in') || stderr.include?('session expired') | |
sign_in | |
op(*args) | |
elsif stderr.present? || !status.success? | |
$stdout.puts(stdout) | |
$stderr.puts(stderr) | |
$stderr.puts "op had non-zero exit code #{status.to_s}; exiting" | |
exit(status.to_i) | |
else | |
return stdout | |
end | |
end | |
def each_item | |
items = JSON.parse(op('list', 'items')).sort_by { |i| i['overview']['title'].downcase } | |
items.each do |item| | |
yield item | |
end | |
end | |
def item_urls(item) | |
item.dig('overview', 'URLs')&.map { |u| u['u'] } | |
end | |
def each_http_uri(item) | |
# item.dig('overview', 'URLs')&.each_with_index do |url, index| | |
# uri = URI.parse(url['u']) rescue next | |
# next unless uri.scheme.start_with?('http') | |
# yield uri, "overview.URLs.#{index}.u" | |
# end | |
if (url = item.dig('overview', 'url')) | |
uri = URI.parse(url) rescue nil | |
return unless uri | |
return unless uri.scheme.start_with?('http') | |
yield uri, 'url' | |
end | |
end | |
def test_http(uri) | |
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |client| | |
req = Net::HTTP::Get.new(uri.path.presence || '/', 'User-Agent' => USER_AGENT) | |
res = client.request(req) | |
yield res | |
end | |
end | |
def redirect_location(uri, res) | |
loc = res.header['Location'] | |
loc = loc.first if loc.kind_of?(Array) | |
if loc.start_with?('/') | |
loc = "#{uri.host_portion}#{loc}" | |
end | |
return loc | |
end | |
def handle_redirect(item, uri, field, loc) | |
puts "#{item['overview']['title']} (#{item['uuid']}) redirected" | |
puts " from #{uri}" | |
puts " to #{loc}" | |
puts "Would you like to update the URL? [(y)es/(n)o/[enter custom url]]" | |
response = gets.chomp | |
case response | |
when 'y', 'yes' | |
op 'edit', 'item', item['uuid'], "#{field}=#{loc}" | |
when 'n', 'no' | |
# do nothing | |
else | |
op 'edit', 'item', item['uuid'], "#{field}=#{response}" | |
end | |
end | |
def handle_error(item, uri, field, error) | |
puts "#{item['overview']['title']} (#{item['uuid']}) could not be loaded" | |
puts " #{uri}" | |
puts " -> #{error}" | |
puts "Would you like to archive this entry? [(y)es/(n)o]" | |
response = gets.chomp | |
case response | |
when 'y', 'yes' | |
op 'delete', 'item', item['uuid'], '--archive' | |
when 'n', 'no' | |
# do nothing | |
else | |
handle_error(item, uri, field, error) | |
end | |
end | |
each_item do |item| | |
puts "#{item['uuid']}: #{item['overview']['title']}" if verbose | |
each_http_uri(item) do |uri, field| | |
puts " #{uri}" if verbose | |
test_http(uri) do |res| | |
if res.kind_of?(Net::HTTPSuccess) | |
# do nothing | |
elsif res.kind_of?(Net::HTTPRedirection) | |
loc = redirect_location(uri, res) | |
puts " -> #{loc}" if verbose | |
handle_redirect(item, uri, field, loc) | |
else | |
puts " ! #{res.class}" if verbose | |
handle_error(item, uri, field, res.class) | |
end | |
end | |
rescue => err | |
puts " !! #{err.class} #{err}" if verbose | |
handle_error(item, uri, field, err) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment