w-stdlib 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []