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] }, '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], 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)' ).
-
.pkg_manager ⇒ Object
- Supported Method Parameters
PWN::Setup.pkg_manager.
Class Method Details
.authors ⇒ Object
- Author(s)
0day Inc. support@0dayinc.com
548 549 550 551 552 |
# File 'lib/pwn/setup.rb', line 548 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)' )
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 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 |
# File 'lib/pwn/setup.rb', line 323 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, | 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 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?, native_gems_missing: gem_missing, toolchain_missing: bin_missing, 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)' )
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 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 |
# File 'lib/pwn/setup.rb', line 398 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
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 |
# File 'lib/pwn/setup.rb', line 556 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' ) # 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 --list-profiles pwn setup --dry-run --profile net #{self}.authors " end |
.list_profiles(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::Setup.list_profiles( io: 'optional - IO to write to (default $stdout)' )
481 482 483 484 485 486 487 488 489 490 491 492 493 494 |
# File 'lib/pwn/setup.rb', line 481 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 |
.pkg_manager ⇒ Object
- Supported Method Parameters
PWN::Setup.pkg_manager
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/pwn/setup.rb', line 294 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 |