Module: PWN::Setup

Defined in:
lib/pwn/setup.rb

Overview

PWN::Setup — post-install "doctor" and capability provisioner.

PWN ships as a single gem whose runtime is 100 % autoloaded (lib/pwn.rb) — a plugin whose native gem or OS binary is missing costs nothing until you touch that constant. PWN::Setup is the piece that grows a bare gem install pwn into a fully-armed host after the gem is installed, instead of before via a chain of bash scripts that assume /opt/pwn, rvmsudo, screen, and MacPorts.

It is the Ruby-native, versioned-with-the-gem replacement for the case $os blocks previously buried in install.sh and packer/provisioners/pwn.sh.

pwn setup                       # doctor (read-only)
pwn setup --check               # same
pwn setup --deps                # install OS headers for EVERY native gem
pwn setup --profile web         # install just what TransparentBrowser/Burp need
pwn setup --profile sdr --yes   # non-interactive
pwn setup --list-profiles

Also exposed as pwn_setup (driver) and pwn --setup[=PROFILE].

Constant Summary collapse

NATIVE_GEMS =

Native-extension Ruby gems → OS headers/libs required to compile them,

per package manager, plus the PWN

constants they unlock.

{
  'pg' => {
    apt: %w[postgresql-server-dev-all], dnf: %w[postgresql-devel],
    pacman: %w[postgresql-libs], brew: %w[postgresql], port: %w[postgresql16-server],
    plugins: %w[PWN::Plugins::DAOPostgres]
  },
  'sqlite3' => {
    apt: %w[libsqlite3-dev], dnf: %w[sqlite-devel],
    pacman: %w[sqlite], brew: %w[sqlite], port: %w[sqlite3],
    plugins: %w[PWN::Plugins::DAOSqlite3]
  },
  'pcaprub' => {
    apt: %w[libpcap-dev], dnf: %w[libpcap-devel],
    pacman: %w[libpcap], brew: %w[libpcap], port: %w[libpcap],
    plugins: %w[PWN::Plugins::Packet extro_packet]
  },
  'packetfu' => {
    # Transitively requires pcaprub (native, libpcap headers) — moved to
    # setup-managed so `gem install pwn` succeeds on a bare host and
    # `pwn setup --profile net` installs libpcap + this + pcaprub together.
    apt: %w[libpcap-dev], dnf: %w[libpcap-devel],
    pacman: %w[libpcap], brew: %w[libpcap], port: %w[libpcap],
    plugins: %w[PWN::Plugins::Packet extro_packet]
  },
  'packetgen' => {
    # Same rationale as packetfu — hard-depends on pcaprub.
    apt: %w[libpcap-dev], dnf: %w[libpcap-devel],
    pacman: %w[libpcap], brew: %w[libpcap], port: %w[libpcap],
    plugins: %w[PWN::Plugins::Packet extro_packet]
  },
  'rmagick' => {
    apt: %w[imagemagick libmagickwand-dev], dnf: %w[ImageMagick-devel],
    pacman: %w[imagemagick], brew: %w[imagemagick], port: %w[imagemagick],
    plugins: %w[PWN::Plugins::OCR PWN::Reports]
  },
  'rtesseract' => {
    apt: %w[tesseract-ocr tesseract-ocr-all], dnf: %w[tesseract],
    pacman: %w[tesseract tesseract-data-eng], brew: %w[tesseract], port: %w[tesseract],
    plugins: %w[PWN::Plugins::OCR extro_vision]
  },
  'ruby-audio' => {
    apt: %w[libsndfile1-dev], dnf: %w[libsndfile-devel],
    pacman: %w[libsndfile], brew: %w[libsndfile], port: %w[libsndfile],
    plugins: %w[PWN::Plugins::Voice PWN::SDR extro_voice]
  },
  'curses' => {
    apt: %w[libncurses-dev], dnf: %w[ncurses-devel],
    pacman: %w[ncurses], brew: %w[ncurses], port: %w[ncurses],
    plugins: %w[PWN::Plugins::REPL]
  },
  'libusb' => {
    apt: %w[libusb-1.0-0-dev], dnf: %w[libusbx-devel],
    pacman: %w[libusb], brew: %w[libusb], port: %w[libusb],
    plugins: %w[PWN::SDR PWN::Plugins::BusPirate]
  },
  'ffi' => {
    apt: %w[libffi-dev], dnf: %w[libffi-devel],
    pacman: %w[libffi], brew: %w[libffi], port: %w[libffi],
    plugins: %w[PWN::FFI]
  },
  'nokogiri' => {
    apt: %w[libxml2-dev libxslt1-dev], dnf: %w[libxml2-devel libxslt-devel],
    pacman: %w[libxml2 libxslt], brew: %w[libxml2], port: %w[libxml2 libxslt],
    plugins: %w[PWN::Plugins::TransparentBrowser PWN::WWW]
  },
  'oily_png' => {
    apt: %w[build-essential], dnf: %w[gcc make],
    pacman: %w[base-devel], brew: %w[gcc], port: %w[gcc12],
    plugins: %w[PWN::Plugins::ScannableCodes]
  },
  'eventmachine' => {
    apt: %w[libssl-dev], dnf: %w[openssl-devel],
    pacman: %w[openssl], brew: %w[openssl], port: %w[openssl],
    plugins: %w[PWN::Plugins::Sock PWN::Plugins::IRC]
  },
  'gruff' => {
    apt: %w[imagemagick libmagickwand-dev], dnf: %w[ImageMagick-devel],
    pacman: %w[imagemagick], brew: %w[imagemagick], port: %w[imagemagick],
    plugins: %w[PWN::Reports]
  },
  # Not a native extension, but gated by required_ruby_version >= 4.0 on
  # rubygems.org — so it cannot be a hard runtime dependency of pwn while
  # pwn.gemspec advertises `>= 3.3`. Managed here instead so
  # `gem install pwn` succeeds on distro rubies (3.3/3.4) and `pwn setup`
  # installs it post-hoc where possible. See install-matrix.yml.
  'meshtastic' => {
    apt: %w[], dnf: %w[], pacman: %w[], brew: %w[], port: %w[],
    min_ruby: '4.0.0',
    plugins: %w[PWN::Plugins::REPL#pwn-mesh]
  }
}.freeze
TOOLCHAIN =

External binaries PWN wraps → OS packages that provide them. (Extrospection.probe_toolchain already knows how to detect these; this table knows how to install them.)

{
  'nmap' => {
    apt: %w[nmap], dnf: %w[nmap], pacman: %w[nmap], brew: %w[nmap], port: %w[nmap],
    plugins: %w[PWN::Plugins::NmapIt]
  },
  'chromium' => {
    apt: %w[chromium], dnf: %w[chromium], pacman: %w[chromium], brew: %w[chromium], port: %w[chromium],
    plugins: %w[PWN::Plugins::TransparentBrowser extro_verify extro_watch]
  },
  'geckodriver' => {
    apt: %w[firefox-esr], dnf: %w[firefox], pacman: %w[geckodriver], brew: %w[geckodriver], port: %w[geckodriver],
    plugins: %w[PWN::Plugins::TransparentBrowser]
  },
  'msfconsole' => {
    apt: %w[metasploit-framework], dnf: %w[metasploit-framework], pacman: %w[metasploit],
    brew: %w[metasploit], port: %w[],
    plugins: %w[PWN::Plugins::Metasploit]
  },
  'burpsuite' => {
    apt: %w[burpsuite], dnf: %w[], pacman: %w[burpsuite], brew: %w[burp-suite], port: %w[],
    plugins: %w[PWN::Plugins::BurpSuite]
  },
  'zaproxy' => {
    apt: %w[zaproxy], dnf: %w[], pacman: %w[zaproxy], brew: %w[zap], port: %w[],
    plugins: %w[PWN::Plugins::Zaproxy]
  },
  'sqlmap' => {
    apt: %w[sqlmap], dnf: %w[sqlmap], pacman: %w[sqlmap], brew: %w[sqlmap], port: %w[],
    plugins: %w[PWN::Plugins::Fuzz]
  },
  'tor' => {
    apt: %w[tor], dnf: %w[tor], pacman: %w[tor], brew: %w[tor], port: %w[tor],
    plugins: %w[PWN::Plugins::Tor]
  },
  'tshark' => {
    apt: %w[tshark], dnf: %w[wireshark-cli], pacman: %w[wireshark-cli],
    brew: %w[wireshark], port: %w[wireshark],
    plugins: %w[PWN::Plugins::Packet extro_packet]
  },
  'tcpdump' => {
    apt: %w[tcpdump], dnf: %w[tcpdump], pacman: %w[tcpdump], brew: %w[tcpdump], port: %w[tcpdump],
    plugins: %w[PWN::Plugins::Packet extro_packet]
  },
  'tesseract' => {
    apt: %w[tesseract-ocr], dnf: %w[tesseract], pacman: %w[tesseract],
    brew: %w[tesseract], port: %w[tesseract],
    plugins: %w[PWN::Plugins::OCR extro_vision]
  },
  'zbarimg' => {
    apt: %w[zbar-tools], dnf: %w[zbar], pacman: %w[zbar], brew: %w[zbar], port: %w[zbar],
    plugins: %w[PWN::Plugins::ScannableCodes extro_vision]
  },
  'espeak-ng' => {
    apt: %w[espeak-ng], dnf: %w[espeak-ng], pacman: %w[espeak-ng],
    brew: %w[espeak-ng], port: %w[espeak-ng],
    plugins: %w[PWN::Plugins::Voice extro_voice]
  },
  'sox' => {
    apt: %w[sox], dnf: %w[sox], pacman: %w[sox], brew: %w[sox], port: %w[sox],
    plugins: %w[PWN::Plugins::Voice PWN::SDR]
  },
  'gqrx' => {
    apt: %w[gqrx-sdr], dnf: %w[gqrx], pacman: %w[gqrx], brew: %w[gqrx], port: %w[gqrx],
    plugins: %w[PWN::SDR extro_rf_tune]
  },
  'rtl_sdr' => {
    apt: %w[rtl-sdr], dnf: %w[rtl-sdr], pacman: %w[rtl-sdr], brew: %w[librtlsdr], port: %w[rtl-sdr],
    plugins: %w[PWN::SDR PWN::FFI::RTLSdr]
  },
  'hackrf_info' => {
    apt: %w[hackrf], dnf: %w[hackrf], pacman: %w[hackrf], brew: %w[hackrf], port: %w[hackrf],
    plugins: %w[PWN::SDR PWN::FFI::HackRF]
  },
  'SoapySDRUtil' => {
    apt: %w[soapysdr-tools], dnf: %w[SoapySDR], pacman: %w[soapysdr],
    brew: %w[soapysdr], port: %w[SoapySDR],
    plugins: %w[PWN::SDR PWN::FFI::SoapySDR]
  },
  'multimon-ng' => {
    apt: %w[multimon-ng], dnf: %w[multimon-ng], pacman: %w[multimon-ng],
    brew: %w[multimon-ng], port: %w[],
    plugins: %w[PWN::SDR::Decoder]
  },
  'dot' => {
    apt: %w[graphviz], dnf: %w[graphviz], pacman: %w[graphviz],
    brew: %w[graphviz], port: %w[graphviz],
    plugins: %w[documentation/diagrams]
  },
  'adb' => {
    apt: %w[adb], dnf: %w[android-tools], pacman: %w[android-tools],
    brew: %w[android-platform-tools], port: %w[],
    plugins: %w[PWN::Plugins::Android]
  },
  'baresip' => {
    apt: %w[baresip], dnf: %w[baresip], pacman: %w[baresip], brew: %w[baresip], port: %w[],
    plugins: %w[PWN::Plugins::BareSIP extro_telecomm]
  },
  'whois' => {
    apt: %w[whois], dnf: %w[whois], pacman: %w[whois], brew: %w[whois], port: %w[whois],
    plugins: %w[extro_osint]
  },
  'jq' => {
    apt: %w[jq], dnf: %w[jq], pacman: %w[jq], brew: %w[jq], port: %w[jq],
    plugins: %w[extro_osint]
  }
}.freeze
PROFILES =

Capability profiles — how you get the ergonomics of pwn-full without shipping a second gem. pwn setup --profile <name>.

{
  core: {
    desc: '~/.pwn bootstrap + vault + REPL (always runs on bare `pwn setup --deps`)',
    gems: %w[ffi nokogiri curses eventmachine],
    bins: %w[]
  },
  ai: {
    desc: 'verify at least one AI engine key/oauth in ~/.pwn/pwn.yaml',
    gems: %w[],
    bins: %w[jq]
  },
  web: {
    desc: 'TransparentBrowser · BurpSuite · Zaproxy · extro_verify · extro_watch',
    gems: %w[nokogiri eventmachine],
    bins: %w[chromium geckodriver burpsuite zaproxy sqlmap tor]
  },
  net: {
    desc: 'NmapIt · Packet · extro_packet · extro_osint',
    gems: %w[pcaprub packetfu packetgen],
    bins: %w[nmap tshark tcpdump whois jq]
  },
  db: {
    desc: 'DAOPostgres · DAOSqlite3 · DAOMongo',
    gems: %w[pg sqlite3],
    bins: %w[]
  },
  sdr: {
    desc: 'PWN::SDR · GQRX · FFI DSP backends · extro_rf_tune',
    gems: %w[ruby-audio libusb ffi],
    bins: %w[gqrx rtl_sdr hackrf_info SoapySDRUtil multimon-ng sox]
  },
  vision: {
    desc: 'OCR · ScannableCodes · Reports · extro_vision',
    gems: %w[rmagick rtesseract oily_png gruff],
    bins: %w[tesseract zbarimg dot]
  },
  voice: {
    desc: 'PWN::Plugins::Voice · extro_voice',
    gems: %w[ruby-audio],
    bins: %w[espeak-ng sox]
  },
  exploit: {
    desc: 'Metasploit · sqlmap',
    gems: %w[],
    bins: %w[msfconsole sqlmap]
  },
  hardware: {
    desc: 'Serial · BusPirate · Android · BareSIP · extro_serial · extro_telecomm',
    gems: %w[libusb],
    bins: %w[adb baresip]
  },
  full: {
    desc: 'everything above',
    gems: NATIVE_GEMS.keys,
    bins: TOOLCHAIN.keys
  }
}.freeze
OK =
"\e[32mok\e[0m"
MISS =
"\e[31mMISSING\e[0m"

Class Method Summary collapse

Class Method Details

.authorsObject

Author(s)

0day Inc. support@0dayinc.com



737
738
739
740
741
# File 'lib/pwn/setup.rb', line 737

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

.check(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Setup.check( io: 'optional - IO to write the report to (default $stdout)' )



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/pwn/setup.rb', line 337

public_class_method def self.check(opts = {})
  io = opts[:io] || $stdout
  pm = pkg_manager
  os = detect_os_attr(attr: :type, fallback: RbConfig::CONFIG['host_os'])
  arch = detect_os_attr(attr: :arch, fallback: RbConfig::CONFIG['host_cpu'])

  io.puts "PWN v#{PWN::VERSION} · ruby #{RUBY_VERSION} · #{os} #{arch} · pkg-manager: #{pm[:key]}"
  io.puts

  # ~/.pwn -----------------------------------------------------------
  pwn_root  = File.join(Dir.home, '.pwn')
  pwn_yaml  = File.join(pwn_root, 'pwn.yaml')
  pwn_dec   = File.join(pwn_root, 'pwn.yaml.decryptor')
  root_ok   = File.directory?(pwn_root)
  root_note = root_ok ? "(#{Dir.children(pwn_root).length} entries)" : '(will be created on first `pwn` launch)'
  io.puts "#{'~/.pwn/'.ljust(26)} #{root_ok ? OK : MISS}   #{root_note}"
  yaml_ok   = File.file?(pwn_yaml)
  dec_state = File.file?(pwn_dec) ? 'present' : 'MISSING'
  yaml_note = yaml_ok ? "(encrypted, decryptor #{dec_state})" : '(run `pwn` once, then `pwn-vault` to configure)'
  io.puts "#{'~/.pwn/pwn.yaml'.ljust(26)} #{yaml_ok ? OK : MISS}   #{yaml_note}"
  engine = ai_active_engine
  key_ok = ai_key_configured?(engine: engine)
  engine_note = if engine
                  "#{engine} #{key_ok ? '(key set)' : '(no key — set via `pwn-vault`)'}"
                else
                  '(no ai.active in pwn.yaml)'
                end
  io.puts "#{'AI engine'.ljust(26)} #{key_ok ? OK : MISS}   #{engine_note}"
  io.puts

  # Native gems ------------------------------------------------------
  io.puts 'Ruby extensions'
  gem_missing = []
  NATIVE_GEMS.each do |gem_name, meta|
    ok = gem_loadable?(name: gem_name)
    gem_missing << gem_name unless ok
    need = ok ? '' : "(needs: #{Array(meta[pm[:key]]).join(' ')})"
    io.puts "  #{gem_name.to_s.ljust(14)} #{ok ? OK : MISS}  #{need.ljust(42)}#{Array(meta[:plugins]).join(', ')}"
  end
  io.puts

  # Toolchain --------------------------------------------------------
  io.puts "#{'External toolchain'.ljust(46)} used by"
  bin_missing = []
  TOOLCHAIN.each do |bin, meta|
    path = which(name: bin)
    bin_missing << bin if path.empty?
    shown_path = path.empty? ? '' : path[0, 24]
    io.puts "  #{bin.to_s.ljust(16)} #{path.empty? ? MISS : OK}   #{shown_path.ljust(24)} #{Array(meta[:plugins]).join(', ')}"
  end
  io.puts

  # ~/.pwn state ----------------------------------------------------
  migrate = nil
  if defined?(PWN::Migrate)
    migrate = PWN::Migrate.check(io: io)
    io.puts
  end

  total   = NATIVE_GEMS.size + TOOLCHAIN.size
  missing = gem_missing.size + bin_missing.size
  io.puts "#{total - missing} / #{total} capabilities usable · #{missing} degraded"
  io.puts
  unless missing.zero?
    io.puts 'Run `pwn setup --deps` to install missing OS headers/tools, or'
    io.puts '    `pwn setup --profile <name>` for a subset. See `pwn setup --list-profiles`.'
  end

  { ok: missing.zero? && Array(migrate && migrate[:incompatible]).empty?,
    native_gems_missing: gem_missing, toolchain_missing: bin_missing,
    state: migrate, pkg_manager: pm[:key], os: os, arch: arch }
rescue StandardError => e
  raise e
end

.deps(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Setup.deps( profile: 'optional - one of PROFILES.keys (default :full)', yes: 'optional - non-interactive; assume yes to prompts (default false)', dry_run: 'optional - print commands only, do not execute (default false)', io: 'optional - IO to write to (default $stdout)' )



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
# File 'lib/pwn/setup.rb', line 420

public_class_method def self.deps(opts = {})
  profile = (opts[:profile] || :full).to_sym
  yes     = opts[:yes] ? true : false
  dry_run = opts[:dry_run] ? true : false
  io      = opts[:io] || $stdout

  raise "Unknown profile '#{profile}'. Known: #{PROFILES.keys.join(', ')}" unless PROFILES.key?(profile)

  pm = pkg_manager
  raise 'No supported package manager found (apt / dnf / pacman / brew / port).' if pm[:key] == :unknown

  prof     = PROFILES[profile]
  gems     = Array(prof[:gems])
  # Drop setup-managed gems whose upstream required_ruby_version excludes
  # THIS ruby (e.g. meshtastic >= 4.0 on a distro ruby 3.3) — otherwise
  # `gem install` in the loop below fails and takes the whole profile with it.
  gems = gems.reject do |g|
    floor = NATIVE_GEMS.dig(g, :min_ruby)
    skip  = floor && Gem::Version.new(RUBY_VERSION) < Gem::Version.new(floor)
    io.puts "  \e[33mskip\e[0m #{g} — requires ruby >= #{floor} (running #{RUBY_VERSION})" if skip
    skip
  end
  bins     = Array(prof[:bins])
  os_pkgs  = []
  gems.each { |g| os_pkgs.concat(Array(NATIVE_GEMS.dig(g, pm[:key]))) }
  bins.each { |b| os_pkgs.concat(Array(TOOLCHAIN.dig(b, pm[:key]))) }
  os_pkgs = os_pkgs.reject(&:empty?).uniq

  io.puts "Profile   : #{profile}#{prof[:desc]}"
  io.puts "Pkg mgr   : #{pm[:key]}"
  io.puts "OS pkgs   : #{os_pkgs.empty? ? '(none)' : os_pkgs.join(' ')}"
  io.puts "Ruby exts : #{gems.empty? ? '(none)' : gems.join(' ')}"
  io.puts

  cmds = []
  cmds << "#{pm[:install]} #{os_pkgs.map { |p| Shellwords.escape(p) }.join(' ')}" unless os_pkgs.empty?
  unless gems.empty?
    broken = gems.reject { |g| gem_loadable?(name: g) }
    cmds << "gem pristine #{broken.map { |g| Shellwords.escape(g) }.join(' ')}" unless broken.empty?
    cmds << "gem install #{broken.map { |g| Shellwords.escape(g) }.join(' ')}"  unless broken.empty?
  end

  if cmds.empty?
    io.puts "Nothing to do — profile '#{profile}' is already satisfied."
    return { profile: profile, ran: [], skipped: true }
  end

  io.puts 'Will run:'
  cmds.each { |c| io.puts "  $ #{c}" }
  io.puts

  unless yes || dry_run
    io.print 'Proceed? [y/N] '
    ans = $stdin.gets.to_s.strip.downcase
    return { profile: profile, ran: [], aborted: true } unless %w[y yes].include?(ans)
  end

  ran = []
  cmds.each do |c|
    io.puts "#{c}"
    if dry_run
      ran << { cmd: c, exit: 0, dry_run: true }
    else
      ok = system(c)
      ran << { cmd: c, exit: $CHILD_STATUS && $CHILD_STATUS.exitstatus, ok: ok }
      io.puts "  (exit #{ran.last[:exit]})" unless ok
    end
  end

  io.puts
  io.puts 'Re-checking…'
  io.puts
  result = check(io: io)
  { profile: profile, ran: ran, check: result }
rescue StandardError => e
  raise e
end

.helpObject

Display Usage for this Module



745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
# File 'lib/pwn/setup.rb', line 745

public_class_method def self.help
  puts "USAGE:
    # Read-only doctor — which PWN capabilities are usable on this host?
    #{self}.check

    # Install OS headers + rebuild native gems for a capability profile.
    # profile ∈ #{PROFILES.keys.inspect}
    #{self}.deps(
      profile: 'optional - capability profile (default :full)',
      yes:     'optional - assume yes (non-interactive)',
      dry_run: 'optional - print commands only'
    )


# Opt-in SHIFT+ENTER multi-line support (Terminator plugin + tmux.conf hints).
#{self}.terminal(
  yes:     'optional - assume yes (non-interactive)',
  dry_run: 'optional - print what would happen'
)

    # List capability profiles.
    #{self}.list_profiles

    # Detected package manager (:apt / :dnf / :pacman / :brew / :port).
    #{self}.pkg_manager

    # Data tables — versioned with the gem:
    #{self}::NATIVE_GEMS   # native ext → OS headers → PWN:: constants
    #{self}::TOOLCHAIN     # external bin → OS package → PWN:: constants
    #{self}::PROFILES      # capability profile → gems + bins

    # From the shell:
    pwn setup                       # == check
    pwn setup --check
    pwn setup --deps                # profile :full
    pwn setup --profile web
    pwn setup --profile sdr --yes
    pwn setup --terminal              # opt-in SHIFT+ENTER multi-line (Terminator/tmux)
    pwn setup --list-profiles
    pwn setup --dry-run --profile net
    pwn setup --migrate                  # verify + upgrade ~/.pwn schema
    pwn setup --migrate --fix            # also autofix incompatible files

    #{self}.authors
  "
end

.list_profiles(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Setup.list_profiles( io: 'optional - IO to write to (default $stdout)' )



644
645
646
647
648
649
650
651
652
653
654
655
656
657
# File 'lib/pwn/setup.rb', line 644

public_class_method def self.list_profiles(opts = {})
  io = opts[:io] || $stdout
  io.puts 'Available capability profiles (`pwn setup --profile <name>`):'
  io.puts
  PROFILES.each do |name, meta|
    io.puts "  #{name.to_s.ljust(10)} #{meta[:desc]}"
    io.puts "  #{''.ljust(10)}   gems: #{meta[:gems].join(' ')}" unless meta[:gems].empty?
    io.puts "  #{''.ljust(10)}   bins: #{meta[:bins].join(' ')}" unless meta[:bins].empty?
    io.puts
  end
  PROFILES.keys
rescue StandardError => e
  raise e
end

.migrate(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Setup.migrate( fix: 'optional - also autofix incompatible ~/.pwn files (default false)', dry_run: 'optional - print what WOULD happen (default false)', yes: 'optional - alias for fix:true (CI-friendly)', io: 'optional - IO to write to (default $stdout)' )

Delegate to PWN::Migrate.run — verify every ~/.pwn state file is compatible with THIS pwn release and (optionally) autofix it. A timestamped backup of ~/.pwn is taken first. See PWN::Migrate.help.



671
672
673
674
675
676
677
678
679
# File 'lib/pwn/setup.rb', line 671

public_class_method def self.migrate(opts = {})
  PWN::Migrate.run(
    fix: opts[:fix] || opts[:yes],
    dry_run: opts[:dry_run],
    io: opts[:io] || $stdout
  )
rescue StandardError => e
  raise e
end

.pkg_managerObject

Supported Method Parameters

PWN::Setup.pkg_manager



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/pwn/setup.rb', line 308

public_class_method def self.pkg_manager
  return @pkg_manager if defined?(@pkg_manager) && @pkg_manager

  # Drop the `sudo` prefix when we are already root (Docker / CI
  # containers, `sudo -i`, root shells) or when `sudo` is not
  # installed — otherwise `pwn setup --profile x --yes` dies with
  # `sh: sudo: not found` on stock debian:* / fedora:* images.
  root = begin
    Process.uid.zero?
  rescue StandardError
    false
  end
  sudo = root || !bin?(name: 'sudo') ? '' : 'sudo '

  @pkg_manager =
    if bin?(name: 'apt-get')     then { key: :apt,    install: "#{sudo}apt-get install -y",    sudo: !sudo.empty? }
    elsif bin?(name: 'dnf')      then { key: :dnf,    install: "#{sudo}dnf install -y",        sudo: !sudo.empty? }
    elsif bin?(name: 'pacman')   then { key: :pacman, install: "#{sudo}pacman -S --noconfirm", sudo: !sudo.empty? }
    elsif bin?(name: 'brew')     then { key: :brew,   install: 'brew install',                 sudo: false        }
    elsif bin?(name: 'port')     then { key: :port,   install: "#{sudo}port -N install",       sudo: !sudo.empty? }
    else { key: :unknown, install: nil, sudo: false }
    end
end

.terminal(opts = {}) ⇒ Object

Supported Method Parameters

PWN::Setup.terminal( yes: 'optional - assume yes; non-interactive (default false)', dry_run: 'optional - print what WOULD happen (default false)', io: 'optional - IO to write to (default $stdout)' )

Opt-in installer for SHIFT+ENTER multi-line support in the pwn-ai / pwn-asm REPL. Never runs implicitly — the user invokes it with pwn setup --terminal after PWNMultiLineInput#ensure_vte_shift_enter tells them their emulator can't distinguish Shift+Enter from Enter.

What it does:

1. Prints the two `~/.tmux.conf` lines that persist the runtime
 `tmux set -s` calls PWNMultiLineInput already makes.
2. If running under Terminator (a VTE host), copies
 third_party/terminator/pwn_shift_enter.py to
 ~/.config/terminator/plugins/ and enables the PWNShiftEnter
 plugin in ~/.config/terminator/config — WITH confirmation and
 a .pwnbak backup. This is the only VTE emulator we can fix at
 the GTK layer without patching libvte itself.

Everything here is host-config mutation, which is why it lives in pwn setup and NOT in the REPL's runtime path. See GNOME/vte issues #2601 / #2607 for why VTE can't be fixed with escape sequences alone.



525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
# File 'lib/pwn/setup.rb', line 525

public_class_method def self.terminal(opts = {})
  require 'fileutils'
  io      = opts[:io] || $stdout
  yes     = opts[:yes]
  dry_run = opts[:dry_run]

  io.puts "\n─── pwn setup --terminal ── SHIFT+ENTER multi-line support ───\n\n"

  # ---- tmux persistence hint (always shown) --------------------
  io.puts 'tmux users — persist extended-keys (pwn already sets these at runtime,'
  io.puts 'but they are lost when the tmux server exits). Add to ~/.tmux.conf:'
  io.puts
  io.puts '    set -s  extended-keys on'
  io.puts "    set -as terminal-features 'xterm*:extkeys'"
  io.puts "    set -as terminal-features 'tmux*:extkeys'"
  io.puts "    set -as terminal-features 'screen*:extkeys'"
  io.puts

  # ---- VTE / Terminator plugin (opt-in, mutating) --------------
  vte_ver       = ENV['VTE_VERSION'].to_s
  in_terminator = !ENV['TERMINATOR_UUID'].to_s.empty? || !ENV['TERMINATOR_DBUS_NAME'].to_s.empty?

  unless in_terminator
    if vte_ver.empty?
      io.puts "This terminal already encodes SHIFT+ENTER distinctly (or isn't VTE-based)."
      io.puts 'No further action needed — SHIFT+ENTER should work in pwn-ai / pwn-asm.'
    else
      io.puts "Detected a VTE-#{vte_ver} terminal that is NOT Terminator."
      io.puts 'libvte cannot distinguish SHIFT+ENTER from ENTER (GNOME/vte #2601, #2607)'
      io.puts 'and pwn only ships a GTK-level shim for Terminator. Use ALT+ENTER or a'
      io.puts 'trailing `\` for multi-line, or switch to kitty / wezterm / foot /'
      io.puts 'alacritty / xterm / Konsole / iTerm2 / Terminator for native SHIFT+ENTER.'
    end
    return { installed: false, reason: if in_terminator
                                         nil
                                       else
                                         (vte_ver.empty? ? :not_vte : :vte_not_terminator)
                                       end }
  end

  gem_root   = File.expand_path('../..', __dir__)
  plugin_src = File.join(gem_root, 'third_party', 'terminator', 'pwn_shift_enter.py')
  plugin_dir = File.join(Dir.home, '.config', 'terminator', 'plugins')
  plugin_dst = File.join(plugin_dir, 'pwn_shift_enter.py')
  cfg_path   = File.join(Dir.home, '.config', 'terminator', 'config')

  unless File.exist?(plugin_src)
    io.puts "ERROR: plugin source not found: #{plugin_src}"
    io.puts '       (was pwn installed from a git checkout with third_party/ present?)'
    return { installed: false, reason: :missing_source }
  end

  io.puts 'Terminator detected. pwn ships a GTK plugin that intercepts <Shift>Return'
  io.puts 'BEFORE libvte encodes it and injects `\e[27;2;13~` into the pty, so pwn-ai'
  io.puts 'and pwn-asm see a real SHIFT+ENTER. Installing it will:'
  io.puts
  io.puts "    write   #{plugin_dst}"
  io.puts "    edit    #{cfg_path}   (add PWNShiftEnter to enabled_plugins)"
  io.puts "    backup  #{cfg_path}.pwnbak"
  io.puts

  unless yes
    io.print 'Proceed? [y/N] '
    io.flush
    ans = $stdin.gets.to_s.strip.downcase
    unless %w[y yes].include?(ans)
      io.puts 'aborted.'
      return { installed: false, reason: :declined }
    end
  end

  if dry_run
    io.puts '[dry-run] would mkdir -p, cp, and edit the files above.'
    return { installed: false, reason: :dry_run }
  end

  FileUtils.mkdir_p(plugin_dir)
  FileUtils.cp(plugin_src, plugin_dst)
  io.puts "  #{OK} installed #{plugin_dst}"

  if File.exist?(cfg_path)
    cfg = File.read(cfg_path)
    if cfg.include?('PWNShiftEnter')
      io.puts "  #{OK} PWNShiftEnter already enabled in #{cfg_path}"
    else
      FileUtils.cp(cfg_path, "#{cfg_path}.pwnbak") unless File.exist?("#{cfg_path}.pwnbak")
      new_cfg =
        if cfg =~ /^\s*enabled_plugins\s*=\s*(.*)$/
          cfg.sub(/^(\s*enabled_plugins\s*=\s*)(.*)$/) do
            lead = Regexp.last_match(1)
            rest = Regexp.last_match(2).strip
            rest.empty? ? "#{lead}PWNShiftEnter" : "#{lead}#{rest}, PWNShiftEnter"
          end
        elsif cfg =~ /^\[global_config\]\s*$/
          cfg.sub(/^\[global_config\]\s*$/) { |m| "#{m}\n  enabled_plugins = PWNShiftEnter" }
        else
          "[global_config]\n  enabled_plugins = PWNShiftEnter\n#{cfg}"
        end
      File.write(cfg_path, new_cfg)
      io.puts "  #{OK} enabled PWNShiftEnter in #{cfg_path} (backup: config.pwnbak)"
    end
  else
    FileUtils.mkdir_p(File.dirname(cfg_path))
    File.write(cfg_path, "[global_config]\n  enabled_plugins = PWNShiftEnter\n[plugins]\n")
    io.puts "  #{OK} wrote #{cfg_path}"
  end

  io.puts
  io.puts '>>> RESTART Terminator (close ALL its windows) for SHIFT+ENTER to take effect. <<<'
  { installed: true, plugin: plugin_dst, config: cfg_path }
rescue StandardError => e
  raise e
end