whowas 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +14 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +44 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/apis/splunk.rb +52 -0
- data/lib/generators/whowas/api_generator.rb +13 -0
- data/lib/generators/whowas/install_generator.rb +9 -0
- data/lib/generators/whowas/recipe_generator.rb +13 -0
- data/lib/generators/whowas/search_method_generator.rb +13 -0
- data/lib/generators/whowas/templates/api.rb +37 -0
- data/lib/generators/whowas/templates/initializer.rb +40 -0
- data/lib/generators/whowas/templates/recipe.rb +14 -0
- data/lib/generators/whowas/templates/search_method.rb +62 -0
- data/lib/whowas.rb +62 -0
- data/lib/whowas/api.rb +30 -0
- data/lib/whowas/configuration.rb +32 -0
- data/lib/whowas/errors.rb +21 -0
- data/lib/whowas/formattable.rb +23 -0
- data/lib/whowas/middleware.rb +23 -0
- data/lib/whowas/parsable.rb +45 -0
- data/lib/whowas/recipes.rb +32 -0
- data/lib/whowas/searchable.rb +26 -0
- data/lib/whowas/validatable.rb +45 -0
- data/lib/whowas/version.rb +3 -0
- data/whowas.gemspec +38 -0
- metadata +187 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 23b13b6e3c208d2ed5d32406ee185c7d9042edb9
|
4
|
+
data.tar.gz: 6d5ebc269ba4ad75720d5b71fe80c7c85d2bb01d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2d7885df76e22f2f4f4d3fe213387a001992ad446c43656971d19ffeeee67e1f2a8968294c8ffec0341926b4bc83456e24c1d4fd8732e851795a4beb21867606
|
7
|
+
data.tar.gz: 0354e38f5de7f29de16db613edc10ad9e2dcf83ace7e19135b88125bd9c2efbccda0d56d0aba34df1f2b666a6187e24c40671e07d02a389b18ce42635d051525
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.2.2
|
4
|
+
install:
|
5
|
+
- travis_retry bundle install
|
6
|
+
sudo: false
|
7
|
+
cache: bundler
|
8
|
+
|
9
|
+
addons:
|
10
|
+
code_climate:
|
11
|
+
repo_token: bd0689fbbea3942f98293e557a67ab25be700ef489f28e680a74e3761b50c3fa
|
12
|
+
|
13
|
+
script:
|
14
|
+
- bundle exec rspec spec
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Jess Frisch, Tufts University
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# Whowas
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/TuftsUniversity/whowas) [](https://codeclimate.com/github/TuftsUniversity/whowas/coverage) [](https://travis-ci.org/TuftsUniversity/whowas) [](https://gemnasium.com/TuftsUniversity/whowas)
|
4
|
+
|
5
|
+
## Description
|
6
|
+
|
7
|
+
Whowas is a simple tool for chaining third-party API searches together. It is called "Whowas" because it was initially developed to answer the question, "Who was the person (username) using this IP address at that time?"
|
8
|
+
|
9
|
+
Whowas can be used to match any piece of data with another through an arbitrary number of API searches, where the result of each search becomes the input to the next search.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'whowas'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install whowas
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
Please see the [wiki](https://github.com/TuftsUniversity/whowas/wiki) for basic usage and other documentation.
|
30
|
+
|
31
|
+
## Development
|
32
|
+
|
33
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
34
|
+
|
35
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
36
|
+
|
37
|
+
## Contributing
|
38
|
+
|
39
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/TuftsUniversity/whowas.
|
40
|
+
|
41
|
+
## License
|
42
|
+
|
43
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
44
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "whowas"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/apis/splunk.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require "splunk-sdk-ruby"
|
2
|
+
|
3
|
+
module Whowas
|
4
|
+
class Splunk
|
5
|
+
include Whowas::Api
|
6
|
+
|
7
|
+
@@connection = nil
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
# Whowas.splunk_config is configured via define_setting in the initializer.
|
12
|
+
# See configuration.rb for more information.
|
13
|
+
def self.connection(config: Whowas.splunk_config)
|
14
|
+
@@connection ||= ::Splunk::connect(config)
|
15
|
+
rescue => e
|
16
|
+
raise Whowas::Errors::ServiceUnavailable, e
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate(input)
|
20
|
+
(input[:query] &&
|
21
|
+
!input[:query].empty? &&
|
22
|
+
input[:offset].is_a?(Integer) &&
|
23
|
+
DateTime.parse(input[:timestamp]) &&
|
24
|
+
true) ||
|
25
|
+
(raise Whowas::Errors::InvalidInput, "Invalid input for Splunk API")
|
26
|
+
end
|
27
|
+
|
28
|
+
def format(input)
|
29
|
+
input = {
|
30
|
+
query: "search #{input[:query]}",
|
31
|
+
args: {
|
32
|
+
earliest_time: format_timestamp(input[:timestamp], input[:offset]),
|
33
|
+
latest_time: format_timestamp(input[:timestamp], 0.1)
|
34
|
+
}
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def format_timestamp(timestamp, offset)
|
39
|
+
(DateTime.parse(timestamp).to_time + offset).strftime("%Y-%m-%dT%H:%M:%S.%L%z")
|
40
|
+
end
|
41
|
+
|
42
|
+
def search_api(input)
|
43
|
+
puts input
|
44
|
+
stream = self.class.connection.create_export(input[:query], input[:args])
|
45
|
+
if results = ::Splunk::MultiResultsReader.new(stream).first.first
|
46
|
+
results["_raw"]
|
47
|
+
else
|
48
|
+
""
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Whowas
|
2
|
+
class ApiGenerator < Rails::Generators::NamedBase
|
3
|
+
source_root File.expand_path("../templates", __FILE__)
|
4
|
+
|
5
|
+
def create
|
6
|
+
copy_file "api.rb", "app/apis/#{file_name}.rb"
|
7
|
+
end
|
8
|
+
|
9
|
+
def rename_class
|
10
|
+
gsub_file "app/apis/#{file_name}.rb", /MyApi/, name.camelize
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Whowas
|
2
|
+
class RecipeGenerator < Rails::Generators::NamedBase
|
3
|
+
source_root File.expand_path("../templates", __FILE__)
|
4
|
+
|
5
|
+
def create
|
6
|
+
copy_file "recipe.rb", "app/recipes/#{file_name}.rb"
|
7
|
+
end
|
8
|
+
|
9
|
+
def rename_method
|
10
|
+
gsub_file "app/recipes/#{file_name}.rb", /name_this_recipe/, name.underscore
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Whowas
|
2
|
+
class SearchMethodGenerator < Rails::Generators::NamedBase
|
3
|
+
source_root File.expand_path("../templates", __FILE__)
|
4
|
+
|
5
|
+
def create
|
6
|
+
copy_file "search_method.rb", "app/search_methods/#{file_name}.rb"
|
7
|
+
end
|
8
|
+
|
9
|
+
def rename_class
|
10
|
+
gsub_file "app/search_methods/#{file_name}.rb", /MySearchMethod/, name.camelize
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Whowas
|
2
|
+
class MyApi
|
3
|
+
# Whowas::Api provides the public interface to your API, accessed through the
|
4
|
+
# "search" instance method.
|
5
|
+
include Whowas::Api
|
6
|
+
|
7
|
+
# All custom API code is defined in the private methods below. Naturally,
|
8
|
+
# you can add private methods as needed for connecting to the API, etc.
|
9
|
+
private
|
10
|
+
|
11
|
+
## Required
|
12
|
+
|
13
|
+
# Sends a search query with provided input to your API and returns results
|
14
|
+
# as a string.
|
15
|
+
def search_api(input)
|
16
|
+
""
|
17
|
+
end
|
18
|
+
|
19
|
+
## Optional
|
20
|
+
|
21
|
+
# Validates input to avoid unnecessary API calls.
|
22
|
+
# MUST return true or raise a Whowas::Errors::InvalidInput error.
|
23
|
+
# Replace "true" with your validation code.
|
24
|
+
def validate(input)
|
25
|
+
true ||
|
26
|
+
(raise Whowas::Errors::InvalidInput, "Invalid input for #{self.class.name}")
|
27
|
+
end
|
28
|
+
|
29
|
+
# Transforms input one last time before API call.
|
30
|
+
# Will be called on input for all search_methods using this API.
|
31
|
+
# For search_method-specific transformations, use the format_input method
|
32
|
+
# in your search_method.
|
33
|
+
def format(input)
|
34
|
+
input
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# WhoWas configuration options
|
2
|
+
Whowas.configuration do |config|
|
3
|
+
## Recipe Table
|
4
|
+
# You MUST define your default recipe and any other recipes you may use
|
5
|
+
# here. Viable keys include "default", "ip_default", "mac_default", and
|
6
|
+
# valid IP addresses or CIDR blocks.
|
7
|
+
#
|
8
|
+
# An example of a full recipe table:
|
9
|
+
#
|
10
|
+
# config.recipe_table = {
|
11
|
+
# "192.168.1.0/24": Whowas.home_wireless,
|
12
|
+
# "10.0.0.0/8": Whowas.internal_wired,
|
13
|
+
# ip_default: Whowas.other_ips,
|
14
|
+
# mac_default: Whowas.search_by_mac,
|
15
|
+
# default: Whowas.other_ips
|
16
|
+
# }
|
17
|
+
config.recipe_table = {}
|
18
|
+
|
19
|
+
## Recipes class
|
20
|
+
# You can change this to a custom defined class if you need to extend the
|
21
|
+
# recipe selection algorithm beyond IP and mac addresses.
|
22
|
+
config.recipe_class = Whowas::Recipes
|
23
|
+
|
24
|
+
## API configuration and credentials
|
25
|
+
# If you are not using a bundled API, you can safely ignore this section.
|
26
|
+
|
27
|
+
# Splunk API configuration
|
28
|
+
# The recommended method is to store your Splunk credentials in environment
|
29
|
+
# variables using a gem such as dotenv.
|
30
|
+
#
|
31
|
+
# config.splunk_config = {
|
32
|
+
# scheme: :https,
|
33
|
+
# host: ENV['SPLUNK_HOST'],
|
34
|
+
# port: ENV['SPLUNK_PORT'],
|
35
|
+
# username: ENV['SPLUNK_USERNAME'],
|
36
|
+
# password: ENV['SPLUNK_PASSWORD']
|
37
|
+
# }
|
38
|
+
#
|
39
|
+
config.splunk_config = {}
|
40
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "middleware"
|
2
|
+
|
3
|
+
module Whowas
|
4
|
+
def self.name_this_recipe
|
5
|
+
# All you have to do is specify the search method classes in the order
|
6
|
+
# they should be called. The output for each search method should match
|
7
|
+
# the input of the next.
|
8
|
+
Middleware::Builder.new do
|
9
|
+
# use MySearchMethod1
|
10
|
+
# use MySearchMethod2
|
11
|
+
# use MySearchMethod3
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Whowas
|
2
|
+
class MySearchMethod
|
3
|
+
# All required public methods are contained in the Middleware package. It
|
4
|
+
# initializes the search method, calls search on the API with the provided
|
5
|
+
# input, and returns the output to the next search method or the caller.
|
6
|
+
#
|
7
|
+
# The Searchable modules (Validatable, Formattable, and Parsable) are
|
8
|
+
# technically optional but in practice necessary to ensure usable input and
|
9
|
+
# outputs.
|
10
|
+
include Whowas::Middleware
|
11
|
+
include Whowas::Searchable
|
12
|
+
|
13
|
+
## API
|
14
|
+
# You MUST set this to the name of a bundled or custom API class.
|
15
|
+
@@api = API_CLASS_HERE
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
## Validatable
|
20
|
+
# Optional but useful to prevent making unnecessary API calls when given
|
21
|
+
# invalid input.
|
22
|
+
|
23
|
+
# Defines required elements of the input hash.
|
24
|
+
# This should be an array containing the required inputs as symbols.
|
25
|
+
def required_inputs
|
26
|
+
[
|
27
|
+
# :ip,
|
28
|
+
# :timestamp
|
29
|
+
]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Validates the values of required inputs.
|
33
|
+
# This should be a hash containing the required input as key, and a lambda
|
34
|
+
# taking input and returning a boolean as value.
|
35
|
+
def input_formats
|
36
|
+
{
|
37
|
+
# timestamp: lambda { |input| DateTime.parse(input) && true rescue false }
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
## Formattable
|
42
|
+
|
43
|
+
# Search method-wide transformations to the input. For example, if all
|
44
|
+
# mac addresses given as input to this search method should use colons as
|
45
|
+
# separators, perform that transformation here.
|
46
|
+
#
|
47
|
+
# API-wide transformations to the input can be made in the API format method.
|
48
|
+
def format_input(input)
|
49
|
+
input
|
50
|
+
end
|
51
|
+
|
52
|
+
## Parsable
|
53
|
+
|
54
|
+
# Extract pieces of the results string from the API using regex to form the
|
55
|
+
# input hash for the next search method or the final result.
|
56
|
+
def output_formats
|
57
|
+
{
|
58
|
+
# username: /User <\K\w*/
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/whowas.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require "middleware"
|
2
|
+
|
3
|
+
require "whowas/api"
|
4
|
+
require "whowas/configuration"
|
5
|
+
require "whowas/errors"
|
6
|
+
require "whowas/formattable"
|
7
|
+
require "whowas/middleware"
|
8
|
+
require "whowas/parsable"
|
9
|
+
require "whowas/recipes"
|
10
|
+
require "whowas/searchable"
|
11
|
+
require "whowas/validatable"
|
12
|
+
require "whowas/version"
|
13
|
+
|
14
|
+
# bundled apis
|
15
|
+
require "apis/splunk"
|
16
|
+
|
17
|
+
|
18
|
+
module Whowas
|
19
|
+
extend Configuration
|
20
|
+
|
21
|
+
# TODO: create a more sophisticated system for creating and retrieving
|
22
|
+
# recipes
|
23
|
+
#
|
24
|
+
# For now, users must provide their own recipes, which is a Middleware stack.
|
25
|
+
# For documentation, see https://github.com/mitchellh/middleware
|
26
|
+
#
|
27
|
+
# To create compatible middleware classes, include Whowas::Middleware.
|
28
|
+
# This will create the necessary initialize and call classes.
|
29
|
+
# It requires the underlying class to have a "#search" function which takes
|
30
|
+
# a hash and returns a string.
|
31
|
+
#
|
32
|
+
# Recipes are automatically selected using the recipe_table attribute, which
|
33
|
+
# can be configured by the user, and the Recipes.select algorithm.
|
34
|
+
#
|
35
|
+
# For more search support, include the Whowas::Searchable module, which adds
|
36
|
+
# validation, formatting, and parsing to the search methods. Check the source
|
37
|
+
# for more documentation.
|
38
|
+
|
39
|
+
define_setting :recipe_table, {}
|
40
|
+
define_setting :recipe_class, Whowas::Recipes
|
41
|
+
|
42
|
+
# configuration defaults
|
43
|
+
define_setting :splunk_config, nil
|
44
|
+
|
45
|
+
def self.search(input)
|
46
|
+
recipe = recipe_class.select(input) || Whowas.recipe_table[:default]
|
47
|
+
env = {input: input, results: []}
|
48
|
+
recipe.call(env)
|
49
|
+
env
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Uses the recipe_table hash to match up the input to the correct recipe type.
|
55
|
+
# If there is an IP address in the input, it tries to match the IP address
|
56
|
+
# against any keys that are valid CIDR blocks.
|
57
|
+
# If there is a mac address, it looks for the key :mac.
|
58
|
+
# Otherwise, it falls back to :default.
|
59
|
+
def select_recipe(input)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
data/lib/whowas/api.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Whowas
|
2
|
+
module Api
|
3
|
+
# All APIs use (api_instance).search as the only public method
|
4
|
+
# and follow the pattern below.
|
5
|
+
# Validation and formatting are optional.
|
6
|
+
# search_api must contain the core api search code and return the results.
|
7
|
+
def search(input)
|
8
|
+
validate(input)
|
9
|
+
input = format(input)
|
10
|
+
search_api(input)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# default methods for APIs to use or override
|
16
|
+
#:nocov:
|
17
|
+
def validate(input)
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def format(input)
|
22
|
+
input
|
23
|
+
end
|
24
|
+
|
25
|
+
# MUST be overridden (core search functionality)
|
26
|
+
def search_api(input)
|
27
|
+
raise Errors::SubclassResponsibility
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Whowas
|
2
|
+
# This is just a simple way to allow gem users to define their own variables
|
3
|
+
# on whatever this module extends -- mainly the Whowas module itself.
|
4
|
+
#
|
5
|
+
# see the following article:
|
6
|
+
# https://www.viget.com/articles/easy-gem-configuration-variables-with-defaults
|
7
|
+
module Configuration
|
8
|
+
def configuration
|
9
|
+
yield self
|
10
|
+
end
|
11
|
+
|
12
|
+
def define_setting(name, default = nil)
|
13
|
+
class_variable_set("@@#{name}", default)
|
14
|
+
|
15
|
+
define_class_method "#{name}=" do |value|
|
16
|
+
class_variable_set("@@#{name}", value)
|
17
|
+
end
|
18
|
+
|
19
|
+
define_class_method name do
|
20
|
+
class_variable_get("@@#{name}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def define_class_method(name, &block)
|
27
|
+
(class << self; self; end).instance_eval do
|
28
|
+
define_method name, &block
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Whowas
|
2
|
+
# A base error class for Whowas. Most of the errors that will be thrown
|
3
|
+
# from Whowas will inherit from this class.
|
4
|
+
class Error < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
module Errors
|
8
|
+
# Will be thrown when input is invalid.
|
9
|
+
class InvalidInput < Whowas::Error
|
10
|
+
end
|
11
|
+
|
12
|
+
# Will be thrown when an external service is unavailable.
|
13
|
+
class ServiceUnavailable < Whowas::Error
|
14
|
+
end
|
15
|
+
|
16
|
+
# Will be thrown when a subclass or including class doesn't define
|
17
|
+
# a required method.
|
18
|
+
class SubclassResponsibility < Whowas::Error
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Whowas
|
2
|
+
module Formattable
|
3
|
+
def format(input)
|
4
|
+
format_input(input)
|
5
|
+
end
|
6
|
+
|
7
|
+
# :nocov:
|
8
|
+
private
|
9
|
+
# This is a hook for the including class to use to modify the input before
|
10
|
+
# the api gets it.
|
11
|
+
#
|
12
|
+
# For example, when searching in Splunk, the Firewall class (which includes
|
13
|
+
# Searchable) must create the query string from the ip and port, plus any
|
14
|
+
# other parameters like the Splunk index it wants to search.
|
15
|
+
#
|
16
|
+
# By default, this method just returns the arguments given, but note that
|
17
|
+
# most classes _will have_ to modify the input to make it usable by the
|
18
|
+
# API modification method.
|
19
|
+
def format_input(input)
|
20
|
+
input
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Whowas
|
2
|
+
module Middleware
|
3
|
+
def initialize(app = nil)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
env[:results] ||= Array.new
|
9
|
+
if results = self.search(env[:input])
|
10
|
+
if results[:raw] && !results[:raw].empty?
|
11
|
+
env[:results] << {results[:method] => results[:raw]}
|
12
|
+
env[:input] = results[:input].merge({timestamp: env[:input][:timestamp]})
|
13
|
+
@app.call(env) unless !@app
|
14
|
+
else
|
15
|
+
env[:results] << { results[:method] => "No results found." }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
env[:results]
|
19
|
+
rescue Whowas::Error => e
|
20
|
+
env[:error] = e
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Whowas
|
2
|
+
module Parsable
|
3
|
+
# Parses a raw result into a hash including the raw result and
|
4
|
+
# other data extracted from the raw result to be used as potential inputs
|
5
|
+
# for other searches.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
# Given the raw string "I am a string, but I also have an IP -- 192.168.1.1"
|
9
|
+
# The result should be:
|
10
|
+
# {
|
11
|
+
# raw: "I am a string, but I also have an IP -- 192.168.1.1",
|
12
|
+
# input: { ip: "192.168.1.1" }
|
13
|
+
# }
|
14
|
+
def parse(raw)
|
15
|
+
{
|
16
|
+
raw: raw,
|
17
|
+
input: parse_for_input(raw),
|
18
|
+
method: self.class.name
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
# :nocov:
|
23
|
+
private
|
24
|
+
|
25
|
+
def parse_for_input(raw)
|
26
|
+
if raw && !raw.empty?
|
27
|
+
output_formats.map { |k, v| [k, raw[v]] }.to_h.delete_if{ |k, v| v.nil? }
|
28
|
+
else
|
29
|
+
{}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# A hook for each including class to define the input names and formats the
|
34
|
+
# parser should look for in the raw result.
|
35
|
+
#
|
36
|
+
# This should be a hash of name -> regex, for example:
|
37
|
+
# {
|
38
|
+
# ip: /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/,
|
39
|
+
# username: /Username=\K\w*/
|
40
|
+
# }
|
41
|
+
def input_formats
|
42
|
+
{}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "ipaddr"
|
2
|
+
|
3
|
+
module Whowas
|
4
|
+
class Recipes
|
5
|
+
def self.select(input)
|
6
|
+
if input[:ip]
|
7
|
+
select_by_ip(input[:ip])
|
8
|
+
elsif input[:mac]
|
9
|
+
Whowas.recipe_table[:mac_default]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def self.select_by_ip(ip)
|
16
|
+
ip = IPAddr.new(ip)
|
17
|
+
|
18
|
+
results = Whowas.recipe_table.select do |key, value|
|
19
|
+
if subnet = (IPAddr.new(key.to_s) rescue nil)
|
20
|
+
subnet.include?(ip)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
if results.empty?
|
25
|
+
Whowas.recipe_table[:ip_default]
|
26
|
+
else
|
27
|
+
results.values.first
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Whowas
|
2
|
+
module Searchable
|
3
|
+
@@api = nil
|
4
|
+
|
5
|
+
# The including class *must* set the api class in a class constant.
|
6
|
+
def api
|
7
|
+
@@api || (raise Errors::SubclassResponsibility)
|
8
|
+
end
|
9
|
+
|
10
|
+
# extend the including class with the searchable sub-modules
|
11
|
+
def self.included klass
|
12
|
+
klass.class_eval do
|
13
|
+
include Validatable
|
14
|
+
include Formattable
|
15
|
+
include Parsable
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def search(input)
|
20
|
+
validate(input)
|
21
|
+
input = format(input)
|
22
|
+
result = api.new.search(input)
|
23
|
+
parse(result)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Whowas
|
2
|
+
module Validatable
|
3
|
+
# Checks for required inputs and input formats
|
4
|
+
# that the API will need to process the search.
|
5
|
+
#
|
6
|
+
# It does *not* matter if there are other, non-required parameters
|
7
|
+
# in the input hash; they will be ignored later.
|
8
|
+
def validate(input)
|
9
|
+
(check_exists(required_inputs, input) &&
|
10
|
+
check_format(input_formats, input)) ||
|
11
|
+
(raise Errors::InvalidInput, "Invalid input for #{self.class.name}")
|
12
|
+
end
|
13
|
+
|
14
|
+
# :nocov:
|
15
|
+
private
|
16
|
+
|
17
|
+
# hooks for required_inputs and input_formats
|
18
|
+
# must be set by including class
|
19
|
+
def required_inputs
|
20
|
+
[]
|
21
|
+
end
|
22
|
+
|
23
|
+
def input_formats
|
24
|
+
{}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Required keys must exist in the input hash and must have a non-nil,
|
28
|
+
# non-empty value.
|
29
|
+
def check_exists(required, input)
|
30
|
+
required.inject(true) do |result, key|
|
31
|
+
input[key] && result
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Format is a lambda defined in the api class which returns true or false.
|
36
|
+
# This allows for flexible format checking:
|
37
|
+
# e.g. regex for ip addresses and DateTime.parse calls for timestamps
|
38
|
+
def check_format(formats, input)
|
39
|
+
formats.inject(true) do |result, (key, format)|
|
40
|
+
format.call(input[key]) && result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
data/whowas.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'whowas/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "whowas"
|
8
|
+
spec.version = Whowas::VERSION
|
9
|
+
spec.authors = ["Jess Frisch"]
|
10
|
+
spec.email = ["jess.frisch@tufts.edu"]
|
11
|
+
|
12
|
+
spec.summary = %q{Match an IP address and timestamp to a username.}
|
13
|
+
spec.homepage = "https://github.com/TuftsUniversity/whowas"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
17
|
+
# delete this section to allow pushing this gem to any host.
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
20
|
+
else
|
21
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
22
|
+
end
|
23
|
+
|
24
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
spec.bindir = "exe"
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_dependency "middleware"
|
30
|
+
spec.add_dependency "splunk-sdk-ruby"
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
33
|
+
spec.add_development_dependency "codeclimate-test-reporter"
|
34
|
+
spec.add_development_dependency "pry-byebug"
|
35
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
36
|
+
spec.add_development_dependency "rspec"
|
37
|
+
spec.add_development_dependency "simplecov"
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: whowas
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jess Frisch
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-06-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: middleware
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: splunk-sdk-ruby
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.10'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.10'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: codeclimate-test-reporter
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry-byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '10.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '10.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: simplecov
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
- jess.frisch@tufts.edu
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".rspec"
|
134
|
+
- ".travis.yml"
|
135
|
+
- Gemfile
|
136
|
+
- LICENSE.txt
|
137
|
+
- README.md
|
138
|
+
- Rakefile
|
139
|
+
- bin/console
|
140
|
+
- bin/setup
|
141
|
+
- lib/apis/splunk.rb
|
142
|
+
- lib/generators/whowas/api_generator.rb
|
143
|
+
- lib/generators/whowas/install_generator.rb
|
144
|
+
- lib/generators/whowas/recipe_generator.rb
|
145
|
+
- lib/generators/whowas/search_method_generator.rb
|
146
|
+
- lib/generators/whowas/templates/api.rb
|
147
|
+
- lib/generators/whowas/templates/initializer.rb
|
148
|
+
- lib/generators/whowas/templates/recipe.rb
|
149
|
+
- lib/generators/whowas/templates/search_method.rb
|
150
|
+
- lib/whowas.rb
|
151
|
+
- lib/whowas/api.rb
|
152
|
+
- lib/whowas/configuration.rb
|
153
|
+
- lib/whowas/errors.rb
|
154
|
+
- lib/whowas/formattable.rb
|
155
|
+
- lib/whowas/middleware.rb
|
156
|
+
- lib/whowas/parsable.rb
|
157
|
+
- lib/whowas/recipes.rb
|
158
|
+
- lib/whowas/searchable.rb
|
159
|
+
- lib/whowas/validatable.rb
|
160
|
+
- lib/whowas/version.rb
|
161
|
+
- whowas.gemspec
|
162
|
+
homepage: https://github.com/TuftsUniversity/whowas
|
163
|
+
licenses:
|
164
|
+
- MIT
|
165
|
+
metadata:
|
166
|
+
allowed_push_host: https://rubygems.org
|
167
|
+
post_install_message:
|
168
|
+
rdoc_options: []
|
169
|
+
require_paths:
|
170
|
+
- lib
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
requirements: []
|
182
|
+
rubyforge_project:
|
183
|
+
rubygems_version: 2.4.8
|
184
|
+
signing_key:
|
185
|
+
specification_version: 4
|
186
|
+
summary: Match an IP address and timestamp to a username.
|
187
|
+
test_files: []
|