Module: PWN::Reports

Defined in:
lib/pwn/reports.rb,
lib/pwn/reports/csv.rb,
lib/pwn/reports/pdf.rb,
lib/pwn/reports/xml.rb,
lib/pwn/reports/fuzz.rb,
lib/pwn/reports/html.rb,
lib/pwn/reports/json.rb,
lib/pwn/reports/sast.rb,
lib/pwn/reports/phone.rb,
lib/pwn/reports/sarif.rb,
lib/pwn/reports/markdown.rb,
lib/pwn/reports/engagement.rb,
lib/pwn/reports/uri_buster.rb,
lib/pwn/reports/ai_red_team.rb,
lib/pwn/reports/html_footer.rb,
lib/pwn/reports/html_header.rb

Overview

This file, using the autoload directive loads Report modules into memory only when they're needed. For more information, see: http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html

Defined Under Namespace

Modules: AIRedTeam, CSV, Engagement, Fuzz, HTML, HTMLFooter, HTMLHeader, JSON, Markdown, PDF, Phone, SARIF, SAST, URIBuster, XML

Class Method Summary collapse

Class Method Details

.attack_chains(opts = {}) ⇒ Object

Connected explicit references only; references never imply a severity boost.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/pwn/reports.rb', line 67

public_class_method def self.attack_chains(opts = {})
  rows = Array(opts[:findings]).map { |row| stringify_keys(hash: row) }
  by_id = rows.to_h { |row| [row['id'].to_s, row] }
  adjacency = Hash.new { |hash, key| hash[key] = [] }
  rows.each do |row|
    id = row['id'].to_s
    refs = Array(row['attack_chain_refs'] || row['chain_refs'] || row['chain_parent_id'])
    refs.each do |ref|
      ref = ref.to_s
      next if id.empty? || ref == id || !by_id.key?(ref)
      next unless row['engagement_id'].to_s == by_id[ref]['engagement_id'].to_s

      adjacency[id] << ref
      adjacency[ref] << id
    end
  end
  seen = []
  adjacency.keys.sort.filter_map do |id|
    next if seen.include?(id)

    group = []
    pending = [id]
    until pending.empty?
      current = pending.shift
      next if group.include?(current)

      group << current
      pending.concat(adjacency[current])
    end
    seen.concat(group)
    ranks = %w[info low medium high critical]
    severity = group.map { |key| by_id[key]['severity'].to_s }.max_by { |value| ranks.index(value) || -1 }
    { finding_ids: group.sort, combined_severity: severity,
      rationale: 'Maximum recorded constituent severity. No automatic escalation; linking is not proof of combined exploitability.' }
  end
end

.authorsObject



113
114
115
# File 'lib/pwn/reports.rb', line 113

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <[email protected]>\n"
end

.helpObject



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/pwn/reports.rb', line 117

public_class_method def self.help
  puts "USAGE:
    # Run resolve path and return its result
    #{self}.resolve_path(
      path: 'required - filesystem path to read or write',
      ext: 'optional - ext value consumed by #resolve_path',
      dir_path: 'optional - dir path value consumed by #resolve_path',
      report_name: 'optional - report name value consumed by #resolve_path'
    )

    # Compose explicit same-engagement references; never invent severity escalation.
    #{self}.attack_chains(findings: 'required - Array of finding hashes')

    # Run report payload and return its result
    #{self}.report_payload(
      results_hash: 'optional - results hash value consumed by #report_payload',
      title: 'optional - title value consumed by #report_payload',
      executive_summary: 'optional - executive summary value consumed by #report_payload'
    )

    # Print the AUTHOR(S) string for this module.
    #{self}.authors
  "
  constants.sort
end

.report_payload(opts = {}) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/pwn/reports.rb', line 42

public_class_method def self.report_payload(opts = {})
  raw = opts[:results_hash]
  raw = {} unless raw.is_a?(Hash)
  title = (
    opts[:title] ||
    raw[:title] || raw['title'] ||
    raw[:report_name] || raw['report_name'] ||
    'PWN Report'
  ).to_s
  summary = (
    opts[:executive_summary] ||
    raw[:executive_summary] || raw['executive_summary']
  ).to_s
  findings = raw[:findings] || raw['findings'] || raw[:data] || raw['data'] || []
  findings = [] unless findings.is_a?(Array)
  {
    title: title,
    executive_summary: summary,
    findings: findings.map { |row| stringify_keys(hash: row) },
    attack_chains: attack_chains(findings: findings),
    raw: raw
  }
end

.resolve_path(opts = {}) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/pwn/reports.rb', line 26

public_class_method def self.resolve_path(opts = {})
  path = opts[:path].to_s
  ext = opts[:ext].to_s.sub(/\A\./, '')
  unless path.empty?
    FileUtils.mkdir_p(File.dirname(path)) unless File.dirname(path).to_s.empty? || File.dirname(path) == '.'
    return path
  end

  dir = opts[:dir_path].to_s
  dir = '.' if dir.empty?
  FileUtils.mkdir_p(dir)
  name = opts[:report_name].to_s
  name = File.basename(Dir.pwd) if name.empty?
  File.join(dir, "#{name}.#{ext}")
end