├── .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 | 
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 | 
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"
--------------------------------------------------------------------------------