w-stdlib 0.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f7cb8e2bf20665a3a90bfa7cdfbf7db465f5867bce893d78783b3ccfa38d245a
4
+ data.tar.gz: 3ce32ae202b1a5f77fa3cf90e97295d775a4c2fa6487daa92fbb8a4f6c6830fa
5
+ SHA512:
6
+ metadata.gz: 43bae6362746dd9b6bbddf3fa1fc74a8272be4db2e15df2fb65daaa9905a7938b2a8f5faf45228e29348678bd2d390a3258eedae83218ff37ee68e1c5a4ed4b5
7
+ data.tar.gz: 7e8447678675d9df0e2c8fea625833c893a471403df9713aac377b06a86afbfbe34d2697e86b237e860708b2bd96073528fe427d8ff31b1311a42bf1c323873f
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'parallel'
4
+ gem 'activesupport'
5
+ gem 'http'
6
+ gem 'netaddr'
7
+ gem 'aws-sdk', '~> 3'
8
+ gem 'jwt'
9
+ gem 'sys-proctable'
10
+ gem 'rspec'
data/lib/aws.rb ADDED
@@ -0,0 +1,84 @@
1
+ require_relative '../lib/glob'
2
+ require_relative '../lib/core_ext/string'
3
+ require_relative '../lib/core_ext/object'
4
+ require_relative '../lib/core_ext/hash'
5
+
6
+ using StringExt
7
+ using ObjectExt
8
+ using HashExt
9
+
10
+
11
+ module IAM
12
+ class AssumeRoleStatement
13
+ def initialize(s)
14
+ @effect = s['Effect']
15
+ @action = s['Action']
16
+ @principal = s['Principal'].map_vals(&:lift_array) # principals can be string or string[]
17
+ end
18
+
19
+ def allow?
20
+ @effect.downcase == 'allow'
21
+ end
22
+
23
+ def deny?
24
+ @effect.downcase == 'deny'
25
+ end
26
+
27
+ def assume_role?
28
+ @action.include? 'sts:AssumeRole'
29
+ end
30
+
31
+ def applies_to_principal?(arn)
32
+ @principal.fetch('AWS', []).any? { Glob.new(_1).match? arn }
33
+ end
34
+ end
35
+
36
+ class AssumeRolePolicyDocument
37
+ def initialize(doc)
38
+ @doc = doc
39
+ @statements = doc['Statement'].map { AssumeRoleStatement.new _1 }
40
+ end
41
+
42
+ # initializes from a hash of params. it supports docs as
43
+ # url encoded json strings, like the aws api returns or
44
+ # docs that are nested hashes and arrays
45
+ def self.from_role(role)
46
+ doc = role[:assume_role_policy_document]
47
+ doc = role["AssumeRolePolicyDocument"] unless doc
48
+ raise 'invalid role' unless doc
49
+
50
+ doc = doc.url_decode.from_json if doc.is_a? String
51
+
52
+ self.new doc
53
+ end
54
+
55
+ def can_assume?(arn)
56
+ return false if explicitly_denied? arn
57
+ return true if explicitly_allowed? arn
58
+ false
59
+ end
60
+
61
+ private
62
+ def explicitly_denied?(arn)
63
+ @statements.any? { _1.deny? && _1.assume_role? && _1.applies_to_principal?(arn) }
64
+ end
65
+
66
+ def explicitly_allowed?(arn)
67
+ @statements.any? { _1.allow? && _1.assume_role? && _1.applies_to_principal?(arn) }
68
+ end
69
+ end
70
+
71
+ class Role
72
+ attr_reader :doc, :arn
73
+ def initialize(params)
74
+ @params = params
75
+ @arn = params[:arn]
76
+ @arn = params['Arn'] unless @arn
77
+ @doc = AssumeRolePolicyDocument.from_role params
78
+ end
79
+
80
+ def can_assume?(arn)
81
+ @doc.can_assume?(arn)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,24 @@
1
+ class CaseInsensitiveHash < Hash
2
+ def [](key)
3
+ super _insensitive(key)
4
+ end
5
+
6
+ def []=(key, value)
7
+ super _insensitive(key), value
8
+ end
9
+
10
+ # Keeping it DRY.
11
+ protected
12
+
13
+ def _insensitive(key)
14
+ key.respond_to?(:upcase) ? key.upcase : key
15
+ end
16
+
17
+ def self.from_pairs(pairs)
18
+ h = CaseInsensitiveHash.new
19
+ pairs.each do |k, v|
20
+ h[k] = v
21
+ end
22
+ h
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+ require 'parallel'
2
+ require 'etc'
3
+ require_relative '../../lib/core_ext/string'
4
+
5
+ using StringExt
6
+
7
+ module ArrayExt
8
+ refine Array do
9
+ def tail
10
+ self[1..self.length]
11
+ end
12
+
13
+ def but_last
14
+ self.tap(&:pop)
15
+ end
16
+
17
+ def choose
18
+ return first if length < 2
19
+ loop do
20
+ each_with_index { puts "#{_2 + 1}: #{block_given? ? yield(_1) : _1 }" }
21
+ print "> "
22
+ input = STDIN.gets.strip
23
+ break if input.downcase == 'q'
24
+
25
+ i = input.to_i
26
+ next if i <= 0 || i > length
27
+
28
+ return self[i - 1]
29
+ end
30
+ end
31
+
32
+ def p_each(opts=nil, &blk)
33
+ opts = {in_threads: Etc.nprocessors} if opts.nil?
34
+ Parallel.each(self, opts, &blk)
35
+ end
36
+
37
+ def p_map(opts=nil, &blk)
38
+ opts = {in_threads: Etc.nprocessors} if opts.nil?
39
+ Parallel.map(self, opts, &blk)
40
+ end
41
+
42
+ def puts_all
43
+ each { puts _1.to_s }
44
+ end
45
+
46
+ def search(str)
47
+ select do
48
+ s = block_given? ? yield(_1) : _1.to_s
49
+ s.matches_pat? str
50
+ end
51
+ end
52
+
53
+ def to_h_by
54
+ map { [yield(_1), _1] }.to_h
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,38 @@
1
+ require_relative './string'
2
+ require 'aws-sdk-iam'
3
+ require 'aws-sdk-core/arn_parser'
4
+
5
+ using StringExt
6
+
7
+ module IamExt
8
+ refine Aws::IAM::Client do
9
+ def arn_to_policies(arn)
10
+ arn = arn.strip
11
+ type, name = Aws::ARNParser.parse(arn).resource.split('/', 2) rescue return []
12
+ return [] unless name && type
13
+
14
+ policies = []
15
+
16
+ attached_policies = send(:"list_attached_#{type}_policies", {:"#{type}_name" => name}).attached_policies
17
+ attached_policies.each do |p|
18
+ error "fetching #{p.policy_arn}"
19
+ version_id = list_policy_versions(policy_arn: p.policy_arn).versions.find { _1.is_default_version }.version_id
20
+ policy = get_policy_version(policy_arn: p.policy_arn, version_id: version_id).policy_version
21
+ policies << policy.document.url_decode.from_json
22
+ rescue Aws::IAM::Errors::NoSuchEntity
23
+ error "#{a} #{p.policy_name} not found"
24
+ end
25
+
26
+ inline_policies = send(:"list_#{type}_policies", {:"#{type}_name" => name}).policy_names
27
+ inline_policies.each do |policy_name|
28
+ error "fetching #{policy_name}"
29
+ policy = send(:"get_#{type}_policy", {:"#{type}_name" => name, :policy_name => policy_name})
30
+ policies << policy.policy_document.url_decode.from_json
31
+ rescue Aws::IAM::Errors::NoSuchEntity
32
+ error "#{a} #{p.policy_arn} not found"
33
+ end
34
+
35
+ policies
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ require 'digest'
2
+
3
+ module FileExt
4
+ refine File do
5
+ def md5
6
+ Digest::MD5.hexdigest(read)
7
+ end
8
+
9
+ alias_method :name, :path
10
+
11
+ def file?
12
+ File.file? name
13
+ end
14
+
15
+ def strings
16
+ `strings '#{name}'`
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ require_relative '../../lib/core_ext/string'
2
+
3
+ using StringExt
4
+
5
+ module HashExt
6
+ refine Hash do
7
+ def map_vals
8
+ map { [_1, yield(_2)] }.to_h
9
+ end
10
+
11
+ def map_keys
12
+ map { [yield(_1), _2] }.to_h
13
+ end
14
+
15
+ def search(pat)
16
+ select { _1.to_s.matches_pat?(pat) || _2.to_s.matches_pat?(pat) }
17
+ end
18
+
19
+ def search_keys(pat)
20
+ select { _1.to_s.matches_pat?(pat) }
21
+ end
22
+
23
+ def puts_all
24
+ each { puts "#{_1}: #{_2}"}
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ require_relative './string'
2
+
3
+ using StringExt
4
+
5
+ module IOExt
6
+ refine IO do
7
+ def lines
8
+ read.lines.map(&:strip)
9
+ end
10
+
11
+ def json_props_or_lines(*path)
12
+ s = read
13
+ begin
14
+ s.from_json.lift_array.map { _1.dig(*path) }
15
+ rescue JSON::ParserError
16
+ s.lines
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ require 'netaddr'
2
+
3
+ module IPv4NetExt
4
+ refine NetAddr::IPv4Net do
5
+ def each
6
+ (0..len).each { yield nth(_1) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ module ObjectExt
2
+ refine Object do
3
+ def not_nil?
4
+ !self.nil?
5
+ end
6
+
7
+ def _?
8
+ return self if self.not_nil?
9
+ yield
10
+ end
11
+
12
+ def lift_array
13
+ kind_of?(Array) ? self : [self]
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+ require 'openssl'
2
+ require_relative '../case_insensitive_hash'
3
+
4
+ module OpenSSLExt
5
+ refine OpenSSL::X509::Certificate do
6
+ def cn
7
+ subject['CN']
8
+ end
9
+
10
+ def sans
11
+ extensions
12
+ .find { _1.oid == 'subjectAltName' }
13
+ .value
14
+ .split(/,\s+/)
15
+ .map { _1.gsub(/^DNS:/,'') }
16
+ end
17
+
18
+ def dns_names
19
+ [cn] + sans
20
+ end
21
+ end
22
+ refine OpenSSL::X509::Name do
23
+ def to_h
24
+ str = to_s
25
+ if str.start_with?("/")
26
+ # /A=B/C=D format
27
+ CaseInsensitiveHash.from_pairs str[1..-1].split("/").map { _1.split('=', 2) }
28
+ else
29
+ # Comma-separated
30
+ CaseInsensitiveHash.from_pairs str.split(",").map { _1.split('=', 2) }
31
+ end
32
+ end
33
+
34
+ def [](key)
35
+ to_h[key]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,110 @@
1
+ require 'base64'
2
+ require 'json'
3
+ require 'cgi'
4
+
5
+ module StringExt
6
+ refine String do
7
+ def from_json
8
+ JSON.parse self
9
+ end
10
+
11
+ def to_b64
12
+ Base64.encode64 self
13
+ end
14
+
15
+ def to_bool
16
+ self.downcase == 'true'
17
+ end
18
+
19
+ def from_b64
20
+ Base64.decode64 self
21
+ end
22
+
23
+ def url_encode
24
+ CGI.escape self
25
+ end
26
+
27
+ def url_decode
28
+ CGI.unescape self
29
+ end
30
+
31
+ def words
32
+ self.split(/\s+/)
33
+ end
34
+
35
+ def chars
36
+ self.split('')
37
+ end
38
+
39
+ def map(&blk)
40
+ self.chars.map(&blk).join('')
41
+ end
42
+
43
+ def select(&blk)
44
+ self.chars.select(&blk).join('')
45
+ end
46
+
47
+ def reject(&blk)
48
+ self.chars.reject(&blk).join('')
49
+ end
50
+
51
+ def any?(&blk)
52
+ self.chars.any?(&blk)
53
+ end
54
+
55
+ def all?(&blk)
56
+ self.chars.all?(&blk)
57
+ end
58
+
59
+ def grep(pat)
60
+ self.lines.select { _1.matches_pat? pat }
61
+ end
62
+
63
+ def matches_pat?(pat)
64
+ case pat
65
+ when String
66
+ self.include_ignore_case? pat
67
+ when Regexp
68
+ self =~ pat
69
+ else
70
+ raise "matches_pat? only works on String and Regexp. You passed: #{pat.class}"
71
+ end
72
+ end
73
+
74
+ def prefix(p)
75
+ p + self
76
+ end
77
+
78
+ def to_bs
79
+ n, units = self.scan(/([0-9.]+)(B|KB|MB|GB)/).first
80
+ raise "Invalid size: #{self}" unless n && units
81
+ sizes = {
82
+ 'B' => 1,
83
+ 'KB' => 1024,
84
+ 'MB' => 1024 ** 2,
85
+ 'GB' => 1024 ** 3,
86
+ }
87
+ scale = sizes.fetch(units, 1)
88
+ n.to_f*scale
89
+ end
90
+
91
+ def to_regexp
92
+ Regexp.new self
93
+ end
94
+
95
+ def regexp_encode
96
+ Regexp.quote self
97
+ end
98
+
99
+ def include_ignore_case?(s)
100
+ downcase.include? s.downcase
101
+ end
102
+
103
+ def is_numeric?
104
+ Float(self)
105
+ true
106
+ rescue
107
+ false
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,21 @@
1
+ require 'uri'
2
+
3
+ module QueryParamsMixin
4
+ def query_params
5
+ query.nil? ? {} : URI.decode_www_form(query).to_h
6
+ end
7
+
8
+ def with_query_params
9
+ params = query_params
10
+ yield params
11
+ self.query = URI.encode_www_form(params)
12
+ self
13
+ end
14
+ end
15
+
16
+ module URI
17
+ class Generic
18
+ include QueryParamsMixin
19
+ end
20
+ end
21
+
@@ -0,0 +1,59 @@
1
+ require 'http'
2
+ require 'json'
3
+ require 'uri'
4
+ require_relative '../lib/core_ext/uri'
5
+ require_relative '../lib/uri'
6
+ require_relative '../lib/core_ext/string'
7
+
8
+ using StringExt
9
+
10
+ class GithubClient
11
+ def initialize(username='', base_url='https://api.github.com')
12
+ @url = URI.parse base_url
13
+ @username = username
14
+ end
15
+
16
+ def repos
17
+ paginate('/repositories')
18
+ end
19
+
20
+ def repos_for_org(org)
21
+ paginate("/orgs/#{org}/repos")
22
+ end
23
+
24
+ def users
25
+ paginate('/users')
26
+ end
27
+
28
+ def user_perms(owner, repo, username)
29
+ url = @url.clone
30
+ url.path = "/repos/#{owner}/#{repo}/collaborators/#{username}/permission"
31
+ auth = {user: @username, pass: token}
32
+ puts url.to_s
33
+ HTTP.basic_auth(auth)
34
+ .headers({"Accept" => "application/vnd.github.v3+json"})
35
+ .get(url.to_s)
36
+ .to_s
37
+ end
38
+
39
+ private
40
+ def token
41
+ File.read(File.join(ENV['HOME'], '.keys', 'github')).strip
42
+ end
43
+ def paginate(path)
44
+ url = @url.clone
45
+ url.path = path
46
+ Enumerator.new do |e|
47
+ paged_uri = URI::Paged.new(url)
48
+ loop do
49
+ repos = HTTP.get(paged_uri.get).to_s.from_json
50
+ break if repos.empty?
51
+
52
+ repos.each { e.yield _1 }
53
+ rescue JSON::ParserError
54
+ break
55
+ end
56
+ end
57
+ end
58
+ end
59
+
data/lib/glob.rb ADDED
@@ -0,0 +1,13 @@
1
+ require_relative './core_ext/string'
2
+
3
+ using StringExt
4
+
5
+ class Glob
6
+ def initialize(pat)
7
+ @r = pat.regexp_encode.gsub('\*', '.*').to_regexp
8
+ end
9
+
10
+ def match?(s)
11
+ @r.match?(s)
12
+ end
13
+ end
data/lib/prelude.rb ADDED
@@ -0,0 +1,25 @@
1
+ def fatal(msg, code=1, pref='[!] ')
2
+ STDERR.puts pref + msg
3
+ exit code
4
+ end
5
+
6
+ def info(msg, pref='[+] ')
7
+ STDERR.puts pref + msg
8
+ end
9
+
10
+ def error(msg, pref='[!] ')
11
+ STDERR.puts pref + msg
12
+ end
13
+
14
+ def json_file(name)
15
+ JSON.parse File.read(name)
16
+ end
17
+
18
+ def home_join(*paths)
19
+ home = ENV['HOME']
20
+ if paths.empty?
21
+ home
22
+ else
23
+ File.join home, *paths
24
+ end
25
+ end
data/lib/processes.rb ADDED
@@ -0,0 +1,74 @@
1
+ require 'sys/proctable'
2
+ require_relative '../lib/core_ext/file'
3
+ require_relative '../lib/core_ext/string'
4
+ using FileExt
5
+ using StringExt
6
+
7
+ module Processes
8
+ def self.snapshot
9
+ Snapshot.new Sys::ProcTable.ps
10
+ end
11
+
12
+ class Snapshot
13
+ def initialize(procs)
14
+ @procs = procs
15
+ @pid_to_proc = @procs.map { [_1.pid, _1] }.to_h
16
+ end
17
+
18
+ def all
19
+ @procs.map { Processes::Process.new self, _1 }
20
+ end
21
+
22
+ def for_pid(pid)
23
+ proc = @pid_to_proc[pid]
24
+ return nil unless proc
25
+ Processes::Process.new self, proc
26
+ end
27
+
28
+ def search_cmdline(name)
29
+ @procs
30
+ .select { _1.cmdline.downcase.include? name.downcase }
31
+ .reject { _1.pid == ::Process.pid }
32
+ .map { Processes::Process.new self, _1 }
33
+ end
34
+ end
35
+
36
+ class Process
37
+ def initialize(snapshot, proc)
38
+ raise 'proc cant be nil' unless proc
39
+ @snapshot = snapshot
40
+ @proc = proc
41
+ end
42
+
43
+ def parent
44
+ proc = @snapshot.for_pid(self.ppid)
45
+ return nil unless proc
46
+ Processes::Process.new @snapshot, proc
47
+ end
48
+
49
+ def open_files
50
+ raise unless pid.is_numeric?
51
+ `lsof -p #{pid}`
52
+ .lines
53
+ .drop(1)
54
+ .map { _1.strip.scan(/(\/.*$)/).first&.first }
55
+ .reject(&:nil?)
56
+ .uniq
57
+ .select { File.file? _1 }
58
+ .map { File.new _1 }
59
+ end
60
+
61
+ def sig_kill
62
+ raise unless pid.is_numeric?
63
+ `kill -9 #{pid}`
64
+ end
65
+
66
+ def children
67
+ @snapshot.all.select { _1.ppid == pid }.map { Process.new @snapshot, _1 }
68
+ end
69
+
70
+ private def method_missing(symbol, *args)
71
+ @proc.send symbol, *args
72
+ end
73
+ end
74
+ end
data/lib/uri.rb ADDED
@@ -0,0 +1,16 @@
1
+
2
+ module URI
3
+ class Paged
4
+ def initialize(url, start=1, name='page')
5
+ @url = url
6
+ @start = start
7
+ @name = name
8
+ end
9
+
10
+ def get
11
+ @url.with_query_params { _1[@name] = @start }
12
+ @start += 1
13
+ @url
14
+ end
15
+ end
16
+ end
data/lib/w-stdlib.rb ADDED
@@ -0,0 +1 @@
1
+ require ''
data/w-stdlib.gemspec ADDED
@@ -0,0 +1,9 @@
1
+ Gem::Specification.new do |g|
2
+ g.name = 'w-stdlib'
3
+ g.version = '0.0.1'
4
+ g.summary = 'Pre-alpha package that contains abstractions that I find useful when scripting. Not suitable for production use. Breaking changes highly likely even with minor version bumps. Use at your own risk'
5
+ g.description = g.summary
6
+ g.authors = ['will@btlr.dev']
7
+ g.email = 'will@btlr.dev'
8
+ g.files = Dir['lib/**/*'] + %w(Gemfile w-stdlib.gemspec)
9
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: w-stdlib
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - will@btlr.dev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-10-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Pre-alpha package that contains abstractions that I find useful when
14
+ scripting. Not suitable for production use. Breaking changes highly likely even
15
+ with minor version bumps. Use at your own risk
16
+ email: will@btlr.dev
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - Gemfile
22
+ - lib/aws.rb
23
+ - lib/case_insensitive_hash.rb
24
+ - lib/core_ext/array.rb
25
+ - lib/core_ext/aws.rb
26
+ - lib/core_ext/file.rb
27
+ - lib/core_ext/hash.rb
28
+ - lib/core_ext/io.rb
29
+ - lib/core_ext/ipv4net.rb
30
+ - lib/core_ext/object.rb
31
+ - lib/core_ext/openssl.rb
32
+ - lib/core_ext/string.rb
33
+ - lib/core_ext/uri.rb
34
+ - lib/github_client.rb
35
+ - lib/glob.rb
36
+ - lib/prelude.rb
37
+ - lib/processes.rb
38
+ - lib/uri.rb
39
+ - lib/w-stdlib.rb
40
+ - w-stdlib.gemspec
41
+ homepage:
42
+ licenses: []
43
+ metadata: {}
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.0.3
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: Pre-alpha package that contains abstractions that I find useful when scripting.
63
+ Not suitable for production use. Breaking changes highly likely even with minor
64
+ version bumps. Use at your own risk
65
+ test_files: []