Module: PWN::Plugins::ArtifactRegistry

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

Overview

~/.pwn/artifacts// loot index for session_recall.

Constant Summary collapse

ROOT =
File.join(Dir.home, '.pwn', 'artifacts')

Class Method Summary collapse

Class Method Details

.authorsObject



112
113
114
# File 'lib/pwn/plugins/artifact_registry.rb', line 112

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

.get(opts = {}) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/pwn/plugins/artifact_registry.rb', line 45

public_class_method def self.get(opts = {})
  sha = opts[:sha256].to_s
  unless sha.empty?
    dest = File.join(ROOT, 'sha256', sha[0, 2], sha)
    path = dest if File.file?(dest)
  end
  path ||= opts[:path].to_s
  raise 'ERROR: path is required' if path.to_s.empty?
  raise "ERROR: file not found: #{path}" unless File.file?(path)

  { path: path, sha256: Digest::SHA256.file(path).hexdigest, bytes: File.size(path), body: File.binread(path)[0, 65_536] }
end

.helpObject



116
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/pwn/plugins/artifact_registry.rb', line 116

public_class_method def self.help
  puts "USAGE:
    # List host binaries this module expects to be installed.
    #{self}.required_bins

    # Run register and return its result
    #{self}.register(
      session_id: 'optional - pwn-ai session id that produced this artifact',
      path: 'required - filesystem path to read or write',
      kind: 'optional - kind value consumed by #register'
    )

    # Run list and return its result
    #{self}.list(
      session_id: 'optional - pwn-ai session id whose artifacts to list'
    )

    # Read an artifact file and return sha256 plus a body cap.
    #{self}.get(
      path: 'required - filesystem path of a registered artifact',
      sha256: 'optional - content hash used when path is omitted'
    )

    # Store bytes under artifacts/sha256/<h2>/<hash> and append manifest.jsonl.
    #{self}.put(
      bytes: 'optional - raw bytes to store',
      path: 'optional - filesystem path to read when bytes is omitted',
      tool: 'optional - tool name for provenance',
      session_id: 'optional - pwn-ai session id for provenance',
      kind: 'optional - artifact kind',
      tags: 'optional - Array of short labels for this artifact'
    )

    # Page an artifact; grep: searches lines, ref: alias for path.
    #{self}.read_page(
      path: 'required - filesystem path of the artifact',
      ref: 'optional - alias for path',
      offset: 'optional - byte offset (defaults to 0)',
      length: 'optional - byte count (defaults to 16384)',
      mode: 'optional - text, hex, or base64 (defaults to text)',
      grep: 'optional - regex to search instead of paging',
      sha256: 'optional - expected sha256 of the file'
    )

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

.list(opts = {}) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
# File 'lib/pwn/plugins/artifact_registry.rb', line 33

public_class_method def self.list(opts = {})
  sid = (opts[:session_id] || 'default').to_s
  meta = File.join(ROOT, sid, 'index.jsonl')
  return [] unless File.file?(meta)

  File.readlines(meta).filter_map do |ln|
    JSON.parse(ln, symbolize_names: true)
  rescue JSON::ParserError
    nil
  end
end

.put(opts = {}) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/pwn/plugins/artifact_registry.rb', line 92

public_class_method def self.put(opts = {})
  bytes = opts[:bytes]
  src = opts[:path].to_s
  bytes = File.binread(src) if bytes.nil? && File.file?(src)
  raise 'ERROR: bytes or path is required' if bytes.nil?

  sha = Digest::SHA256.hexdigest(bytes)
  dir = File.join(ROOT, 'sha256', sha[0, 2])
  FileUtils.mkdir_p(dir)
  dest = File.join(dir, sha)
  File.binwrite(dest, bytes) unless File.file?(dest)
  meta = File.join(ROOT, 'manifest.jsonl')
  FileUtils.mkdir_p(ROOT)
  File.open(meta, 'a') do |f|
    f.flock(File::LOCK_EX)
    f.puts(JSON.generate(sha256: sha, size: bytes.bytesize, tool: opts[:tool], session: opts[:session_id], kind: opts[:kind], tags: Array(opts[:tags]), created_at: Time.now.utc.iso8601, source_path: src, dest: dest))
  end
  { sha256: sha, path: dest, size: bytes.bytesize, tags: Array(opts[:tags]) }
end

.read_page(opts = {}) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/pwn/plugins/artifact_registry.rb', line 58

public_class_method def self.read_page(opts = {})
  path = (opts[:path] || opts[:ref]).to_s
  raise 'ERROR: path is required' if path.empty?
  raise "ERROR: file not found: #{path}" unless File.file?(path)

  raise 'ERROR: sha256 mismatch' if opts[:sha256].to_s != '' && Digest::SHA256.file(path).hexdigest != opts[:sha256].to_s

  grep = opts[:grep].to_s
  unless grep.empty?
    rx = Regexp.new(grep, Regexp::IGNORECASE)
    hits = []
    File.foreach(path).with_index(1) do |ln, i|
      hits << { line: i, text: ln.chomp } if ln.match?(rx)
      break if hits.length >= 50
    end
    return { path: path, grep: grep, matches: hits }
  end

  offset = opts[:offset].to_i
  length = (opts[:length] || 16_384).to_i
  length = 16_384 if length <= 0
  mode = (opts[:mode] || 'text').to_s
  data = File.binread(path, length, offset).to_s
  case mode
  when 'hex'
    { path: path, offset: offset, bytes: data.bytesize, hex: data.unpack1('H*') }
  when 'base64'
    require 'base64'
    { path: path, offset: offset, bytes: data.bytesize, body: Base64.strict_encode64(data) }
  else
    { path: path, offset: offset, bytes: data.bytesize, body: data.force_encoding('UTF-8').scrub }
  end
end

.register(opts = {}) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/pwn/plugins/artifact_registry.rb', line 18

public_class_method def self.register(opts = {})
  sid = (opts[:session_id] || 'default').to_s
  src = opts[:path].to_s
  raise 'ERROR: path is required' if src.empty?
  raise "ERROR: file not found: #{src}" unless File.file?(src)

  dir = File.join(ROOT, sid)
  FileUtils.mkdir_p(dir)
  dest = File.join(dir, File.basename(src))
  FileUtils.cp(src, dest)
  meta = File.join(dir, 'index.jsonl')
  File.open(meta, 'a') { |f| f.puts(JSON.generate(path: dest, kind: opts[:kind], src: src, at: Time.now.utc.iso8601)) }
  dest
end

.required_binsObject



14
15
16
# File 'lib/pwn/plugins/artifact_registry.rb', line 14

public_class_method def self.required_bins
  []
end