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-fullwithout 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
-
.authors ⇒ Object
- Author(s)
0day Inc.
-
.check(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Setup.check( io: 'optional - IO to write the report to (default $stdout)' ).
-
.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)' ).
-
.help ⇒ Object
Display Usage for this Module.
-
.list_profiles(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Setup.list_profiles( io: 'optional - IO to write to (default $stdout)' ).
-
.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)' ).
-
.pkg_manager ⇒ Object
- Supported Method Parameters
PWN::Setup.pkg_manager.
-
.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)' ).
Class Method Details
.authors ⇒ Object
- Author(s)
0day Inc. support@0dayinc.com
756 757 758 759 760 |
# File 'lib/pwn/setup.rb', line 756 public_class_method def self. "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 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 |
# File 'lib/pwn/setup.rb', line 337 public_class_method def self.check(opts = {}) io = opts[:io] || $stdout profile = opts[:profile]&.to_sym 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, | ok = gem_loadable?(name: gem_name) gem_missing << gem_name unless ok need = ok ? '' : "(needs: #{Array([pm[:key]]).join(' ')})" io.puts " #{gem_name.to_s.ljust(14)} #{ok ? OK : MISS} #{need.ljust(42)} → #{Array([:plugins]).join(', ')}" end io.puts # Toolchain -------------------------------------------------------- io.puts "#{'External toolchain'.ljust(46)} used by" bin_missing = [] TOOLCHAIN.each do |bin, | 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([: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 # When a profile is given (`pwn setup --check --profile core`), :ok is # gated ONLY on that profile's gems/bins so a partially-provisioned host # can still pass its own doctor gate. `packer/provisioners/pwn.sh` and # spec/integration/fresh_install_spec.rb rely on this — a `core` leg of # install-matrix must not fail because gqrx/burpsuite/etc. are absent. gate_gems = gem_missing gate_bins = bin_missing if profile && PROFILES.key?(profile) prof = PROFILES[profile] gate_gems = gem_missing & Array(prof[:gems]) gate_bins = bin_missing & Array(prof[:bins]) io.puts io.puts "Profile '#{profile}' gate: " \ "#{gate_gems.empty? && gate_bins.empty? ? OK : MISS} " \ "(gems: #{gate_gems.empty? ? '—' : gate_gems.join(' ')}, " \ "bins: #{gate_bins.empty? ? '—' : gate_bins.join(' ')})" end { ok: gate_gems.empty? && gate_bins.empty? && 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)' )
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 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 |
# File 'lib/pwn/setup.rb', line 439 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 |
.help ⇒ Object
Display Usage for this Module
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 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 |
# File 'lib/pwn/setup.rb', line 764 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)' )
663 664 665 666 667 668 669 670 671 672 673 674 675 676 |
# File 'lib/pwn/setup.rb', line 663 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, | io.puts " #{name.to_s.ljust(10)} #{[:desc]}" io.puts " #{''.ljust(10)} gems: #{[:gems].join(' ')}" unless [:gems].empty? io.puts " #{''.ljust(10)} bins: #{[:bins].join(' ')}" unless [: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.
690 691 692 693 694 695 696 697 698 |
# File 'lib/pwn/setup.rb', line 690 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_manager ⇒ Object
- 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.
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 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 |
# File 'lib/pwn/setup.rb', line 544 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.('../..', __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 |