├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin └── nmunch ├── lib ├── nmunch.rb └── nmunch │ ├── dissectors │ ├── arp.rb │ ├── base.rb │ └── ip.rb │ ├── mac_oui_lookup.rb │ ├── node.rb │ ├── report.rb │ ├── subscribers │ ├── base.rb │ └── stdout.rb │ └── version.rb ├── mac-prefixes.txt ├── nmunch.gemspec └── spec ├── lib ├── nmunch │ ├── dissectors │ │ ├── arp_spec.rb │ │ ├── base_spec.rb │ │ └── ip_spec.rb │ ├── mac_oui_lookup_spec.rb │ ├── node_spec.rb │ ├── report_spec.rb │ └── subscribers │ │ ├── base_spec.rb │ │ └── stdout_spec.rb └── nmunch_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | results.html 3 | pkg 4 | html 5 | *.gem -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in nmunch.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | nmunch (1.0.0) 5 | methadone (~> 1.2.3) 6 | paint 7 | pcap 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | diff-lcs (1.1.3) 13 | methadone (1.2.3) 14 | bundler 15 | paint (0.8.5) 16 | pcap (0.7.7) 17 | rake (0.9.5) 18 | rspec (2.12.0) 19 | rspec-core (~> 2.12.0) 20 | rspec-expectations (~> 2.12.0) 21 | rspec-mocks (~> 2.12.0) 22 | rspec-core (2.12.1) 23 | rspec-expectations (2.12.0) 24 | diff-lcs (~> 1.1.3) 25 | rspec-mocks (2.12.0) 26 | 27 | PLATFORMS 28 | ruby 29 | 30 | DEPENDENCIES 31 | nmunch! 32 | rake (~> 0.9.2) 33 | rspec 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Michael Henriksen 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nmunch 2 | 3 | ![OM NOM NOM NOM](https://dl.dropbox.com/u/70881769/omnomnomnom.jpg) 4 | 5 | Nmunch is a simple passive network discovery tool to find live nodes just by 6 | listening to [ARP][arp] and broadcast packets on a network interface. 7 | 8 | The main reason why I built this tool was mostly to have an excuse to play around 9 | with the [ruby-pcap gem][ruby-pcap], but it turned out to be quite useful and the 10 | architecture is flexible enough to extend the tool to do pretty much anything 11 | with a discovered node. 12 | 13 | ## Requirements 14 | 15 | - Ruby 1.9 16 | - libpcap () 17 | 18 | ## Installation 19 | 20 | gem install nmunch 21 | 22 | ## Usage 23 | 24 | ![Nmunch munching](https://dl.dropbox.com/u/70881769/nmunch.png "Nmunch munching packets on a network") 25 | 26 | Nmunch is pretty straight-forward and supports two different methods: 27 | 28 | ### Listen on a network device 29 | 30 | nmunch -i 31 | 32 | To find the names of your network devices, use `ifconfig` in a terminal. Usually, 33 | they are called `eth0`, `en1`, etc. 34 | 35 | To stop listening, simply press `Ctrl`+`C` to interrupt. It might take a couple of 36 | seconds before it reacts. 37 | 38 | 39 | ### Analyze a PCAP capture file 40 | 41 | Nmunch can also find nodes from a `pcap` capture file: 42 | 43 | nmunch -f 44 | 45 | ### See all options 46 | 47 | To see all options for Nmunch, type: 48 | 49 | nmunch --help 50 | 51 | ## Extending Nmunch 52 | 53 | I have tried to keep the architecture pretty flexible with a simple *publish/subscribe* pattern for handling the discovered nodes. So if you want all nodes to be pwned by your secret 0day spl0itz, you can write a new node subscriber to do that for you. Have a look at `Nmunch::Subscriber::Stdout` for an example of how to write a node subscriber 54 | 55 | ### *Y U NO capture [insert protocol name] to find more nodes!?!1* 56 | 57 | The current version of Nmunch only looks for [ARP][arp] and broadcast IP packets which captures quite a lot of nodes, but if you have more ideas, you can easily make a new packet dissector to handle it. see `Nmunch::Dissectors::Arp` for an example of how to write a packet dissector. 58 | 59 | ## Roadmap 60 | 61 | This is what I have on the roadmap for Nmunch: 62 | 63 | ### ▢ More tests 64 | ### ▢ Improve documentation 65 | ### ▢ Export results in formats compatible with other tools 66 | ### ▢ Easier way to register custom node subscribers 67 | ### ▢ Extract more interesting information apart from live nodes 68 | ### ▢ Make a [Metasploit] Auxiliary module version of Nmunch 69 | 70 | ## Contributing 71 | 72 | Feel free to file an issue, even if you don't have time to submit a patch. 73 | 74 | Please try to include tests for any patch you submit. If you don't include tests, I'll have to write one, and it'll take longer to get your code in. 75 | 76 | ## License 77 | 78 | Copyright (c) 2012 Michael Henriksen 79 | 80 | MIT License 81 | 82 | Permission is hereby granted, free of charge, to any person obtaining 83 | a copy of this software and associated documentation files (the 84 | "Software"), to deal in the Software without restriction, including 85 | without limitation the rights to use, copy, modify, merge, publish, 86 | distribute, sublicense, and/or sell copies of the Software, and to 87 | permit persons to whom the Software is furnished to do so, subject to 88 | the following conditions: 89 | 90 | The above copyright notice and this permission notice shall be 91 | included in all copies or substantial portions of the Software. 92 | 93 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 94 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 95 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 96 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 97 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 98 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 99 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 100 | 101 | [arp]: http://en.wikipedia.org/wiki/Address_Resolution_Protocol "Address Resolution Protocol" 102 | [ruby-pcap]: https://github.com/ahobson/ruby-pcap 103 | [metasploit]: http://www.metasploit.com/ "Metasploit penetration testing framework" -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | def dump_load_path 2 | puts $LOAD_PATH.join("\n") 3 | found = nil 4 | $LOAD_PATH.each do |path| 5 | if File.exists?(File.join(path,"rspec")) 6 | puts "Found rspec in #{path}" 7 | if File.exists?(File.join(path,"rspec","core")) 8 | puts "Found core" 9 | if File.exists?(File.join(path,"rspec","core","rake_task")) 10 | puts "Found rake_task" 11 | found = path 12 | else 13 | puts "!! no rake_task" 14 | end 15 | else 16 | puts "!!! no core" 17 | end 18 | end 19 | end 20 | if found.nil? 21 | puts "Didn't find rspec/core/rake_task anywhere" 22 | else 23 | puts "Found in #{path}" 24 | end 25 | end 26 | require 'bundler' 27 | require 'rake/clean' 28 | 29 | begin 30 | require 'rspec/core/rake_task' 31 | rescue LoadError 32 | dump_load_path 33 | raise 34 | end 35 | 36 | include Rake::DSL 37 | 38 | Bundler::GemHelper.install_tasks 39 | 40 | RSpec::Core::RakeTask.new do |t| 41 | # Put spec opts in a file named .rspec in root 42 | end 43 | 44 | task :default => [:spec] -------------------------------------------------------------------------------- /bin/nmunch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | require 'methadone' 5 | require 'pcap' 6 | require 'paint' 7 | require 'ipaddr' 8 | require 'nmunch' 9 | 10 | class App 11 | include Methadone::Main 12 | include Methadone::CLILogging 13 | 14 | main do 15 | begin 16 | if !options[:interface] && !options[:file] 17 | help_now!("You must specify an interface or a PCAP file.") 18 | end 19 | 20 | logger.debug("Command-line options: #{options.inspect}") 21 | 22 | logger.debug("Setting up report") 23 | report = Nmunch::Report.new(options) 24 | 25 | logger.debug("Registering STDOUT node subscriber") 26 | report.register_subscriber(Nmunch::Subscribers::Stdout.new(options)) 27 | 28 | # Disable output coloring if specified in options 29 | if options.include?('no-color') 30 | logger.debug("Disabling output coloring") 31 | Paint.mode = 0 32 | end 33 | 34 | if !options.include?('no-banner') 35 | puts Paint[Nmunch.banner, :blue] 36 | puts Paint[" Munching on packets om nom nom nom...\n", :blue] 37 | end 38 | 39 | # Start munching packets and find live nodes... 40 | logger.debug("Munching packets now...") 41 | Nmunch.munch!(options) do |node| 42 | logger.debug("Found node: #{node.ip_address} (#{node.mac_address})") 43 | report.publish_node(node) 44 | end 45 | 46 | logger.debug("Done, printing summary") 47 | puts "\n#{report.summary}" 48 | 49 | rescue Exception => e 50 | if !e.is_a?(Interrupt) 51 | logger.debug("Caught exception!") 52 | logger.debug(e.inspect) 53 | puts Paint[" Warp Core Breach!", :red] 54 | puts Paint[" [#{e.class}] #{e.message}", :red] 55 | else 56 | logger.debug("Caught interrupt") 57 | puts "\n#{report.summary}" 58 | exit! 59 | end 60 | end 61 | end 62 | 63 | on("-i INTERFACE", "--interface", "Interface to munch packets on") 64 | on("-f FILE", "--file", "Path to PCAP file to analyze") 65 | on("--no-prefix-lookup", "Disable MAC prefix lookups") 66 | on("--no-banner", "Don't display awesome Nmunch banner") 67 | on("--no-color", "Don't colorize output") 68 | 69 | description "A passive network discovery tool." 70 | version Nmunch::VERSION 71 | 72 | use_log_level_option 73 | 74 | go! 75 | end 76 | -------------------------------------------------------------------------------- /lib/nmunch.rb: -------------------------------------------------------------------------------- 1 | require "nmunch/version" 2 | require "nmunch/dissectors/base" 3 | require "nmunch/dissectors/ip" 4 | require "nmunch/dissectors/arp" 5 | require "nmunch/subscribers/base" 6 | require "nmunch/subscribers/stdout" 7 | require "nmunch/node" 8 | require "nmunch/report" 9 | require "nmunch/mac_oui_lookup" 10 | 11 | # Public: Main Nmunch module 12 | # 13 | # Acts as a facade in front of the ruby-pcap gem and abstracts away the packet analysis. 14 | module Nmunch 15 | 16 | # Internal: PCAP filter expression to only get ARP and broadcast packets 17 | CAPTURE_FILTER = 'arp or broadcast' 18 | 19 | # Public: Start munching on packets on interface or from PCAP file 20 | # 21 | # options - Hash of command line options 22 | # 23 | # Yields an Nmunch::Node with IP and MAC address information 24 | def self.munch!(options) 25 | create_capture_object(options).each_packet do |packet| 26 | dissector = Nmunch::Dissectors::Base.factory(packet) 27 | yield Nmunch::Node.create_from_dissector(dissector) 28 | end 29 | end 30 | 31 | # Internal: Get the awesome Nmunch ASCII banner 32 | # 33 | # Returns ASCII banner 34 | def self.banner 35 | banner = < Apple 17 | # 18 | # Returns name of organization or Unknown if prefix can't be found 19 | def lookup(mac_address) 20 | load_prefix_file! if @prefixes.nil? 21 | oui = mac_address[0..7].upcase.gsub(':', '').to_sym 22 | @prefixes[oui] || 'Unknown' 23 | end 24 | 25 | private 26 | 27 | # Internal: Load the file with prefixes 28 | # 29 | # Returns nothing 30 | def load_prefix_file! 31 | @prefixes = {} 32 | File.open("#{File.dirname(__FILE__)}/../../mac-prefixes.txt", 'r').each_line do |line| 33 | line.strip! 34 | next if line.empty? || line.start_with?('#', '//') 35 | oui, organization = line.split(' ', 2) 36 | @prefixes[oui.to_sym] = organization 37 | end 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /lib/nmunch/node.rb: -------------------------------------------------------------------------------- 1 | module Nmunch 2 | 3 | # Public: A class to represent a network node 4 | # 5 | # For now it is a simple wrapper around IP and MAC address information but might 6 | # contain more information in the future. 7 | class Node 8 | attr_reader :ip_address, :mac_address 9 | 10 | # Public: Initialize a new node 11 | # 12 | # options - A hash that must contain the keys :ip_address and :mac_address 13 | # 14 | # Returns nothing 15 | def initialize(options) 16 | @ip_address = options[:ip_address] 17 | @mac_address = options[:mac_address] 18 | end 19 | 20 | # Public: Determine if node has a meta address 21 | # 22 | # A node will have a meta address (0.0.0.0) if it has not yet been given a 23 | # DHCP lease 24 | # 25 | # Returns TRUE if meta address and FALSE if not 26 | def meta_address? 27 | ip_address == '0.0.0.0' 28 | end 29 | 30 | # Public: Convert node to string 31 | # 32 | # Returns IP address 33 | def to_s 34 | ip_address 35 | end 36 | 37 | # Public: Compare string version of node 38 | # 39 | # Returns TRUE if same and FALSE if not 40 | def ==(obj) 41 | self.to_s == obj.to_s 42 | end 43 | 44 | # Public: Convenience method to create a node from an Nmunch::Dissector instance 45 | # 46 | # dissector - An instance of Nmunch::Dissector 47 | # 48 | # Returns instance of Nmunch::Node with details from dissector 49 | def self.create_from_dissector(dissector) 50 | self.new(ip_address: dissector.ip_address, mac_address: dissector.mac_address) 51 | end 52 | end 53 | end -------------------------------------------------------------------------------- /lib/nmunch/report.rb: -------------------------------------------------------------------------------- 1 | module Nmunch 2 | 3 | # Public: Nmunch report class 4 | # 5 | # Keeps track of discovered nodes and publishes nodes to registered subscribers 6 | class Report 7 | attr_reader :options 8 | 9 | # Public: Initialize a new report 10 | # 11 | # options - Hash of command line options 12 | def initialize(options) 13 | @options = options 14 | @subscribers = [] 15 | @discovered_nodes = [] 16 | end 17 | 18 | # Public: Register a new subscriber 19 | # 20 | # subscriber - An object that responds to #process(node) 21 | # 22 | # All subscribers will be notified of new nodes and it is up to each subscriber 23 | # to do something meaningful with the node. 24 | # 25 | # Returns nothing 26 | def register_subscriber(subscriber) 27 | @subscribers << subscriber 28 | end 29 | 30 | # Public: Publish a new node to all registered subscribers 31 | # 32 | # node - An instance of Nmunch::Node 33 | # 34 | # Already discovered nodes or nodes with meta addresses (e.g. DHCP requests) 35 | # will be ignored. 36 | # 37 | # Returns nothing 38 | def publish_node(node) 39 | if !node.meta_address? && !@discovered_nodes.include?(node) 40 | @subscribers.each { |s| s.process(node) } 41 | @discovered_nodes << node 42 | end 43 | end 44 | 45 | # Public: Get a summary 46 | # 47 | # Returns a simple summary telling how many live nodes were discovered 48 | # TODO: Make the summary more detailed 49 | def summary 50 | " Discovered #{@discovered_nodes.count} live node(s)" 51 | end 52 | end 53 | end -------------------------------------------------------------------------------- /lib/nmunch/subscribers/base.rb: -------------------------------------------------------------------------------- 1 | module Nmunch 2 | module Subscribers 3 | 4 | # Public: Base subscriber class 5 | class Base 6 | attr_reader :options 7 | 8 | # Public: Initializer 9 | # 10 | # options - A hash of command line options 11 | # 12 | # Returns nothing 13 | def initialize(options) 14 | @options = options 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/nmunch/subscribers/stdout.rb: -------------------------------------------------------------------------------- 1 | module Nmunch 2 | module Subscribers 3 | 4 | # Public: Node subscriber to output node details to STDOUT 5 | # 6 | # This subscriber is default and will always be registered 7 | class Stdout < Nmunch::Subscribers::Base 8 | 9 | # Public: Process a new node 10 | # 11 | # node - An instance of Nmunch::Node 12 | # 13 | # Prints node information to STDOUT 14 | # 15 | # Returns nothing 16 | def process(node) 17 | output = " #{node.ip_address}\t\t#{node.mac_address}" 18 | output << "\t#{Nmunch::MacOuiLookup.instance.lookup(node.mac_address)}" unless options.include?("no-prefix-lookup") 19 | puts Paint[output, :green] 20 | end 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /lib/nmunch/version.rb: -------------------------------------------------------------------------------- 1 | module Nmunch 2 | # Public: Current version of Nmunch 3 | VERSION = "0.1.0" 4 | end 5 | -------------------------------------------------------------------------------- /nmunch.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/nmunch/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Michael Henriksen"] 6 | gem.email = ["michenriksen87@gmail.com"] 7 | gem.description = %q{Nmunch is a passive network discovery tool that finds live network nodes by analyzing ARP and broadcast packets.} 8 | gem.summary = %q{OM NOM NOM NOM} 9 | gem.homepage = "https://github.com/michenriksen/nmunch" 10 | 11 | gem.files = `git ls-files`.split($\) 12 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 13 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 14 | gem.name = "nmunch" 15 | gem.require_paths = ["lib"] 16 | gem.version = Nmunch::VERSION 17 | gem.add_development_dependency('rspec') 18 | gem.add_development_dependency('rake', '>= 0.9.2') 19 | gem.add_dependency('methadone', '~> 1.2.3') 20 | gem.add_dependency('pcap', '~> 0.7.7') 21 | gem.add_dependency('paint') 22 | end 23 | -------------------------------------------------------------------------------- /spec/lib/nmunch/dissectors/arp_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Nmunch::Dissectors::Arp do 4 | let(:arp_pcap_packet) do 5 | packet = stub(Pcap::Packet) 6 | packet.stub(:raw_data).and_return(ARP_PACKET_DATA) 7 | packet.stub(:ip?).and_return(false) 8 | packet 9 | end 10 | 11 | subject { Nmunch::Dissectors::Arp.new(arp_pcap_packet) } 12 | 13 | describe '#ip_address' do 14 | it 'returns correct IP address' do 15 | subject.ip_address.should == '192.168.0.125' 16 | end 17 | end 18 | 19 | describe '#mac_address' do 20 | it 'returns correct MAC address' do 21 | subject.mac_address.should == 'de:ad:be:ef:de:ad' 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /spec/lib/nmunch/dissectors/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Nmunch::Dissectors::Base do 4 | let(:arp_pcap_packet) do 5 | packet = stub(Pcap::Packet) 6 | packet.stub(:raw_data).and_return(ARP_PACKET_DATA) 7 | packet.stub(:ip?).and_return(false) 8 | packet 9 | end 10 | 11 | let(:ip_pcap_packet) do 12 | packet = stub(Pcap::Packet) 13 | packet.stub(:raw_data).and_return(IP_PACKET_DATA) 14 | packet.stub(:ip?).and_return(true) 15 | packet 16 | end 17 | 18 | subject { Nmunch::Dissectors::Base.new(arp_pcap_packet) } 19 | 20 | describe '#pcap_packet' do 21 | it 'returns pcap packet given in initializer' do 22 | subject.pcap_packet.should == arp_pcap_packet 23 | end 24 | end 25 | 26 | describe '#raw_data' do 27 | it 'returns an array of bytes represented in HEX' do 28 | subject.raw_data.should == ARP_PACKET_DATA.unpack('H*').first.scan(/.{2}/) 29 | end 30 | end 31 | 32 | describe '.factory' do 33 | context 'when given ARP packet' do 34 | it 'returns an ARP dissector' do 35 | Nmunch::Dissectors::Base.factory(arp_pcap_packet).should be_a(Nmunch::Dissectors::Arp) 36 | end 37 | end 38 | 39 | context 'when given an IP packet' do 40 | it 'returns an IP dissector' do 41 | Nmunch::Dissectors::Base.factory(ip_pcap_packet).should be_a(Nmunch::Dissectors::Ip) 42 | end 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /spec/lib/nmunch/dissectors/ip_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Nmunch::Dissectors::Ip do 4 | let(:ip_pcap_packet) do 5 | packet = stub(Pcap::Packet) 6 | packet.stub(:raw_data).and_return(IP_PACKET_DATA) 7 | packet.stub(:ip?).and_return(true) 8 | packet 9 | end 10 | 11 | subject { Nmunch::Dissectors::Ip.new(ip_pcap_packet) } 12 | 13 | describe '#ip_address' do 14 | it 'returns correct IP address' do 15 | ip_pcap_packet.should_receive(:ip_src).and_return('192.168.0.125') 16 | subject.ip_address.should == '192.168.0.125' 17 | end 18 | end 19 | 20 | describe '#mac_address' do 21 | it 'returns correct MAC address' do 22 | subject.mac_address.should == 'de:ad:be:ef:de:ad' 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /spec/lib/nmunch/mac_oui_lookup_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Nmunch::MacOuiLookup do 4 | context 'when given an Apple mac address' do 5 | it 'returns Apple' do 6 | Nmunch::MacOuiLookup.instance.lookup('58:b0:35:31:33:73').should == 'Apple' 7 | end 8 | end 9 | 10 | context 'when given a Cisco mac address' do 11 | it 'returns Cisco Systems' do 12 | Nmunch::MacOuiLookup.instance.lookup('00:01:63:42:42:42').should == 'Cisco Systems' 13 | end 14 | end 15 | 16 | context 'when given a mac address with an unknown prefix' do 17 | it 'returns Unknown' do 18 | Nmunch::MacOuiLookup.instance.lookup('de:ad:be:ef:de:ad').should == 'Unknown' 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /spec/lib/nmunch/node_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Nmunch::Node do 4 | subject { Nmunch::Node.new(ip_address: '192.168.0.1', mac_address: 'de:ad:be:ef:de:ad') } 5 | 6 | describe '#ip_address' do 7 | it 'returns correct IP address' do 8 | subject.ip_address.should == '192.168.0.1' 9 | end 10 | end 11 | 12 | describe '#mac_address' do 13 | it 'returns correct MAC address' do 14 | subject.mac_address.should == 'de:ad:be:ef:de:ad' 15 | end 16 | end 17 | 18 | describe '#meta_address?' do 19 | context 'when IP is not a meta address' do 20 | it 'returns false' do 21 | subject.should_not be_meta_address 22 | end 23 | end 24 | 25 | context 'when IP is a meta address' do 26 | it 'returns true' do 27 | subject.stub(:ip_address).and_return('0.0.0.0') 28 | subject.should be_meta_address 29 | end 30 | end 31 | end 32 | 33 | describe '.create_from_dissector' do 34 | let(:dissector) do 35 | dissector = stub 36 | dissector.stub(:ip_address).and_return('192.168.0.1') 37 | dissector.stub(:mac_address).and_return('de:ad:be:ef:de:ad') 38 | dissector 39 | end 40 | subject { Nmunch::Node.create_from_dissector(dissector) } 41 | 42 | it 'returns a node' do 43 | subject.should be_a(Nmunch::Node) 44 | end 45 | 46 | it 'has correct IP address' do 47 | subject.ip_address.should == '192.168.0.1' 48 | end 49 | 50 | it 'has correct MAC address' do 51 | subject.mac_address.should == 'de:ad:be:ef:de:ad' 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /spec/lib/nmunch/report_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class TestSubscriber 4 | def process(node); end 5 | end 6 | 7 | describe Nmunch::Report do 8 | let(:node) { Nmunch::Node.new(ip_address: '192.168.0.1', mac_address: 'de:ad:be:ef:de:ad') } 9 | let(:options) { {interface: 'en1'} } 10 | let(:subscriber) { TestSubscriber.new } 11 | subject { Nmunch::Report.new(options) } 12 | 13 | describe '#publish_node' do 14 | before(:each) do 15 | subject.register_subscriber(subscriber) 16 | end 17 | 18 | it 'delegates the node to the subscriber' do 19 | subscriber.should_receive(:process).with(node) 20 | subject.publish_node(node) 21 | end 22 | 23 | context 'when node has a meta address' do 24 | it 'does not delegate node to subscribers' do 25 | node.stub(:ip_address).and_return('0.0.0.0') 26 | subscriber.should_not_receive(:process) 27 | subject.publish_node(node) 28 | end 29 | end 30 | 31 | context 'when node is already discovered' do 32 | it 'does not delegate node to subscribers' do 33 | subject.publish_node(node) 34 | subscriber.should_not_receive(:process) 35 | subject.publish_node(node) 36 | end 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /spec/lib/nmunch/subscribers/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Nmunch::Subscribers::Base do 4 | let(:options) { {interface: 'en1'} } 5 | subject { Nmunch::Subscribers::Base.new(options) } 6 | 7 | describe '.initialize' do 8 | it 'assigns options to instance variable' do 9 | subject.options.should == options 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /spec/lib/nmunch/subscribers/stdout_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Nmunch::Subscribers::Stdout do 4 | let(:node) { Nmunch::Node.new(ip_address: '192.168.0.1', mac_address: 'de:ad:be:ef:de:ad') } 5 | let(:options) { {interface: 'en1'} } 6 | subject { Nmunch::Subscribers::Stdout.new(options) } 7 | 8 | describe '#process' do 9 | it 'prints node details to STDOUT' do 10 | output = capture_stdout { subject.process(node) } 11 | output.should include('192.168.0.1') 12 | output.should include('de:ad:be:ef:de:ad') 13 | output.should include('Unknown') 14 | end 15 | 16 | context 'when --no-prefix-lookup option is set' do 17 | it 'does not look up mac address prefix' do 18 | options['no-prefix-lookup'] = true 19 | output = capture_stdout { subject.process(node) } 20 | output.should include('192.168.0.1') 21 | output.should include('de:ad:be:ef:de:ad') 22 | output.should_not include('Unknown') 23 | end 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /spec/lib/nmunch_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Nmunch do 4 | describe 'PCAP filter expression' do 5 | it 'allows only ARP and broadcast packets' do 6 | Nmunch::CAPTURE_FILTER.should == 'arp or broadcast' 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | begin 3 | Bundler.setup(:default, :development) 4 | rescue Bundler::BundlerError => e 5 | $stderr.puts e.message 6 | $stderr.puts 'Run `bundle install` to install missing gems' 7 | exit e.status_code 8 | end 9 | 10 | require 'pcap' 11 | require 'ipaddr' 12 | require 'paint' 13 | require 'nmunch' 14 | 15 | RSpec.configure do |config| 16 | config.filter_run :focus => true 17 | config.run_all_when_everything_filtered = true 18 | end 19 | 20 | def capture_stdout 21 | original_stdout = $stdout 22 | $stdout = fake = StringIO.new 23 | begin 24 | yield 25 | ensure 26 | $stdout = original_stdout 27 | end 28 | fake.string 29 | end 30 | 31 | ARP_PACKET_DATA = "\xff\xff\xff\xff\xff\xff\xde\xad\xbe\xef\xde\xad\x08\x06\x00\x01" + 32 | "\x08\x00\x06\x04\x00\x01\xde\xad\xbe\xef\xde\xad\xc0\xa8\x00\x7d" + 33 | "\x00\x00\x00\x00\x00\x00\xc0\xa8\x00\x42" 34 | 35 | IP_PACKET_DATA = "\x7b\x22\x68\x6f\x73\x74\xde\xad\xbe\xef\xde\xad\x20\x32\x36\x37" + 36 | "\x35\x37\x32\x31\x31\x30\x2c\x20\x22\x76\x65\x72\x73\x69\x6f\x6e" + 37 | "\x22\x3a\x20\x5b\x31\x2c\x20\x38\x5d\x2c\x20\x22\x64\x69\x73\x70" + 38 | "\x6c\x61\x79\x6e\x61\x6d\x65\x22\x3a\x20\x22\x32\x36\x37\x35\x37" + 39 | "\x32\x31\x31\x30\x22\x2c\x20\x22\x70\x6f\x72\x74\x22\x3a\x20\x31" + 40 | "\x37\x35\x30\x30\x2c\x20\x22\x6e\x61\x6d\x65\x73\x70\x61\x63\x65" + 41 | "\x73\x22\x3a\x20\x5b\x31\x34\x33\x30\x30\x33\x33\x37\x36\x2c\x20" + 42 | "\x31\x31\x35\x36\x33\x34\x38\x35\x38\x2c\x20\x31\x31\x38\x39\x30" + 43 | "\x36\x37\x30\x31\x5d\x7d" --------------------------------------------------------------------------------