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-
  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.



244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/pwn/plugins/github.rb', line 244

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. [email protected]



285
286
287
288
289
# File 'lib/pwn/plugins/github.rb', line 285

public_class_method def self.authors
  "AUTHOR(S):
    0day Inc. <[email protected]>
  "
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.



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/pwn/plugins/github.rb', line 265

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) }
  # Run api and return its result
  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



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/pwn/plugins/github.rb', line 293

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

    # Run workflow runs and return its result
    #{self}.workflow_runs(
      owner: 'required - repo owner (default PWN::Env[:plugins][:github][:username])',
      repo: 'required - repo name',
      workflow: 'optional - workflow file name or id (e.g. install-matrix.yml). Omit for all runs.',
      params: 'optional - {branch:, status:, per_page:, page:}',
      token: 'optional - token value consumed by #workflow_runs'
    )

    # Run workflow run jobs and return its result
    #{self}.workflow_run_jobs(
      owner: 'required - repo owner (default PWN::Env[:plugins][:github][:username])',
      repo: 'required - repo name',
      run_id: 'required - workflow run id',
      params: 'optional - params value consumed by #workflow_run_jobs',
      token: 'optional - token value consumed by #workflow_run_jobs'
    )

    # Run job log and return its result
    #{self}.job_log(
      owner: 'required - repo owner (default PWN::Env[:plugins][:github][:username])',
      repo: 'required - repo name',
      job_id: 'required - job id from workflow_run_jobs',
      token: 'optional - token value consumed by #job_log'
    )

    # Open a pull request for finding remediations and optional SARIF.
    #{self}.open_fix_pr(
      owner: 'optional - repo owner (default PWN::Env[:plugins][:github][:username])',
      repo: 'required - repo name',
      title: 'required - pull request title',
      head: 'required - branch containing the fix',
      base: 'optional - base branch (defaults to main)',
      body: 'optional - pull request body',
      sarif_path: 'optional - absolute SARIF path mentioned in the body',
      token: 'optional - PAT (default PWN::Env[:plugins][:github][:personal_access_token])'
    )

    # Run api and return its result
    #{self}.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[:plugins][:github][:personal_access_token])',
      raw: 'optional - return raw RestClient::Response instead of parsed JSON (default false)'
    )

    # Thin wrapper around the `gh` CLI with GH_TOKEN injected from PWN::Env so
    #{self}.gh(
      cmd: 'required - gh subcommand string, e.g. run list -R 0dayinc/pwn -L 5 --json databaseId,status',
      token: 'optional - PAT (default PWN::Env[:plugins][:github][:personal_access_token])'
    )

    # Print the AUTHOR(S) string for this module.
    #{self}.authors
  "
  constants.sort
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

.open_fix_pr(opts = {}) ⇒ Object

Open a GitHub pull request that carries finding remediations and optional SARIF.

Raises:

  • (ArgumentError)


201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/pwn/plugins/github.rb', line 201

public_class_method def self.open_fix_pr(opts = {})
  owner = resolve_username(opts)
  repo = opts[:repo].to_s
  title = opts[:title].to_s
  head = opts[:head].to_s
  raise ArgumentError, 'repo is required' if repo.empty?
  raise ArgumentError, 'title is required' if title.empty?
  raise ArgumentError, 'head is required' if head.empty?

  body = opts[:body].to_s
  sarif = opts[:sarif_path].to_s
  body = "#{body}\n\nSARIF: #{sarif}" unless sarif.empty?
  payload = api(
    path: "repos/#{owner}/#{repo}/pulls",
    method: :post,
    token: opts[:token],
    body: {
      title: title,
      body: body,
      head: head,
      base: (opts[:base] || 'main').to_s
    }
  )
  {
    number: payload['number'] || payload[:number],
    html_url: payload['html_url'] || payload[:html_url],
    sarif_path: sarif.empty? ? nil : sarif
  }
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