Module: PWN::Plugins::Github

Defined in:
lib/pwn/plugins/github.rb

Overview

This plugin is used for interacting w/ Github's REST API using the 'rest' browser type of PWN::Plugins::TransparentBrowser.

Credentials are sourced from PWN::Env[:github]:

plugins:
github:
  username: your-gh-login
  personal_access_token: ghp_xxxxxxxxxxxxxxxxxxxx

A PAT with repo + workflow scopes is sufficient for every method here AND for the gh CLI (exported as GH_TOKEN by #self.gh).

Constant Summary collapse

@@logger =
PWN::Plugins::PWNLogger.create

Class Method Summary collapse

Class Method Details

.api(opts = {}) ⇒ Object

Supported Method Parameters

response = PWN::Plugins::Github.api( path: 'required - REST path relative to https://api.github.com (e.g. "repos/0dayinc/pwn/releases/latest")', method: 'optional - :get|:post|:put|:patch|:delete (default :get)', params: 'optional - query params Hash', body: 'optional - request body Hash/Array/String for POST/PUT/PATCH', token: 'optional - PAT (default PWN::Env[:github][:personal_access_token])', raw: 'optional - return raw RestClient::Response instead of parsed JSON (default false)' ) Generic in-process escape hatch for any GitHub REST endpoint not yet wrapped by a named method - parity with gh api <path> without needing the gh binary.



212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/pwn/plugins/github.rb', line 212

public_class_method def self.api(opts = {})
  github_rest_call(
    http_method: opts[:method] || :get,
    rest_call: opts[:path].to_s.delete_prefix('/'),
    params: opts[:params],
    http_body: opts[:body],
    token: opts[:token],
    raw: opts[:raw]
  )
rescue StandardError => e
  raise e
end

.authorsObject

Author(s)

0day Inc. support@0dayinc.com



252
253
254
255
256
# File 'lib/pwn/plugins/github.rb', line 252

public_class_method def self.authors
  "AUTHOR(S):
    0day Inc. <support@0dayinc.com>
  "
end

.download_all_gists(opts = {}) ⇒ Object

Supported Method Parameters

response_json = PWN::Plugins::Github.download_all_gists( username: 'optional - username of gists to backup (default PWN::Env[:github][:username])', target_dir: 'required - target directory to save respective gists' )



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/pwn/plugins/github.rb', line 98

public_class_method def self.download_all_gists(opts = {})
  username = resolve_username(opts).to_s.scrub
  target_dir = opts[:target_dir].to_s.scrub

  raise "ERROR: #{target_dir} Does Not Exist." unless Dir.exist?(target_dir)

  page = 1
  response_json = [{}]
  while response_json.any?
    response_json = github_rest_call(
      rest_call: "users/#{username}/gists",
      params: { page: page }
    )

    Dir.chdir(target_dir)
    response_json.each do |gist_hash|
      clone_dir = gist_hash[:id]
      clone_uri = gist_hash[:git_pull_url]
      next if Dir.exist?(clone_dir)

      print "Cloning: #{clone_uri}..."
      system('git', 'clone', clone_uri)
      puts 'complete.'
    end

    page += 1
  end

  response_json
rescue StandardError => e
  raise e
end

.gh(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Plugins::Github.gh( cmd: 'required - gh subcommand string, e.g. "run list -R 0dayinc/pwn -L 5 --json databaseId,status"', token: 'optional - PAT (default PWN::Env[:github][:personal_access_token])' ) Thin wrapper around the gh CLI with GH_TOKEN injected from PWN::Env so gh auth login is never required.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/pwn/plugins/github.rb', line 233

public_class_method def self.gh(opts = {})
  cmd = opts[:cmd].to_s
  gh_bin = ENV['PATH'].to_s.split(File::PATH_SEPARATOR)
                      .map { |d| File.join(d, 'gh') }
                      .find { |f| File.executable?(f) }
  raise "ERROR: gh CLI not found on PATH - use #{self}.api(path:) instead, or install gh (https://cli.github.com)" unless gh_bin

  token = resolve_token(opts)
  raise 'ERROR: no GitHub token - set plugins.github.personal_access_token in ~/.pwn/pwn.yaml' unless token

  env = { 'GH_TOKEN' => token, 'GH_PROMPT_DISABLED' => '1', 'NO_COLOR' => '1' }
  stdout, stderr, status = Open3.capture3(env, "#{gh_bin} #{cmd}")
  { stdout: stdout, stderr: stderr, exit: status.exitstatus }
rescue StandardError => e
  raise e
end

.helpObject

Display Usage for this Module



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/pwn/plugins/github.rb', line 260

public_class_method def self.help
  puts "USAGE:
    response_json = #{self}.download_all_gists(
      username: 'optional - username of gists to download (default PWN::Env[:plugins][:github][:username])',
      target_dir: 'required - target directory to save respective gists'
    )

    runs = #{self}.workflow_runs(
      owner: 'optional - default PWN::Env[:plugins][:github][:username]',
      repo: 'required - repo name',
      workflow: 'optional - e.g. install-matrix.yml',
      params: 'optional - {branch:, status:, per_page:}'
    )

    jobs = #{self}.workflow_run_jobs(
      owner: 'optional', repo: 'required', run_id: 'required'
    )

    log = #{self}.job_log(
      owner: 'optional', repo: 'required', job_id: 'required'
    )

    json = #{self}.api(
      path: 'required - e.g. repos/0dayinc/pwn/releases/latest',
      method: 'optional - :get|:post|:put|:patch|:delete',
      params: 'optional', body: 'optional', raw: 'optional'
    )

    #{self}.gh(cmd: 'run view <run_id> -R <owner>/<repo> --log-failed')

    #{self}.authors
  "
end

.job_log(opts = {}) ⇒ Object

Supported Method Parameters

log_text = PWN::Plugins::Github.job_log( owner: 'required - repo owner (default PWN::Env[:github][:username])', repo: 'required - repo name', job_id: 'required - job id from workflow_run_jobs' ) NOTE: log download REQUIRES an authenticated token — unauthenticated 403s.



185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/pwn/plugins/github.rb', line 185

public_class_method def self.job_log(opts = {})
  owner = resolve_username(opts)
  repo = opts[:repo]
  job_id = opts[:job_id]

  github_rest_call(
    rest_call: "repos/#{owner}/#{repo}/actions/jobs/#{job_id}/logs",
    token: opts[:token],
    raw: true
  ).body
rescue StandardError => e
  raise e
end

.workflow_run_jobs(opts = {}) ⇒ Object

Supported Method Parameters

jobs = PWN::Plugins::Github.workflow_run_jobs( owner: 'required - repo owner (default PWN::Env[:github][:username])', repo: 'required - repo name', run_id: 'required - workflow run id' )



163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/pwn/plugins/github.rb', line 163

public_class_method def self.workflow_run_jobs(opts = {})
  owner = resolve_username(opts)
  repo = opts[:repo]
  run_id = opts[:run_id]

  github_rest_call(
    rest_call: "repos/#{owner}/#{repo}/actions/runs/#{run_id}/jobs",
    params: opts[:params],
    token: opts[:token]
  )
rescue StandardError => e
  raise e
end

.workflow_runs(opts = {}) ⇒ Object

Supported Method Parameters

runs = PWN::Plugins::Github.workflow_runs( owner: 'required - repo owner (default PWN::Env[:github][:username])', repo: 'required - repo name', workflow: 'optional - workflow file name or id (e.g. install-matrix.yml). Omit for all runs.', params: 'optional - status:, per_page:, page:' )



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/pwn/plugins/github.rb', line 139

public_class_method def self.workflow_runs(opts = {})
  owner = resolve_username(opts)
  repo = opts[:repo]
  workflow = opts[:workflow]
  params = opts[:params] || { per_page: 30 }

  rest_call = if workflow
                "repos/#{owner}/#{repo}/actions/workflows/#{workflow}/runs"
              else
                "repos/#{owner}/#{repo}/actions/runs"
              end

  github_rest_call(rest_call: rest_call, params: params, token: opts[:token])
rescue StandardError => e
  raise e
end