Permalink
Cannot retrieve contributors at this time
#!/usr/bin/ruby env | |
#Encoding: UTF-8 | |
# Written by: @porterhau5 - 10/22/17 | |
require 'net/http' | |
require 'uri' | |
require 'json' | |
require 'optparse' | |
require 'set' | |
# Recommended to create the following indexes: | |
# CREATE INDEX ON :Group(wave) | |
# CREATE INDEX ON :User(wave) | |
# CREATE INDEX ON :Computer(wave) | |
# CREATE INDEX ON :Group(blacklist) | |
# CREATE INDEX ON :User(blacklist) | |
# CREATE INDEX ON :Computer(blacklist) | |
# Show indexes with ":schema" | |
# This method changes text color to a supplied integer value which correlates to Ruby's color representation | |
def colorize(text, color_code) | |
"\e[#{color_code}m#{text}\e[0m" | |
end | |
# This method changes text color to red | |
def red(text) | |
colorize(text, 31) | |
end | |
# This method changes text color to blue | |
def blue(text) | |
colorize(text, 34) | |
end | |
# This method changes text color to green | |
def green(text) | |
colorize(text, 32) | |
end | |
def examples() | |
puts "Find all owned Domain Admins:" | |
puts "MATCH (n:Group) WHERE n.name =~ '.*DOMAIN ADMINS.*' WITH n MATCH p=(n)<-[r:MemberOf*1..]-(m) WHERE exists(m.owned) RETURN nodes(p),relationships(p)" | |
puts "" | |
puts "Find Shortest Path from owned node to Domain Admins:" | |
puts "MATCH p=shortestPath((n)-[*1..]->(m)) WHERE exists(n.owned) AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p" | |
puts "" | |
puts "List all directly owned nodes:" | |
puts "MATCH (n) WHERE exists(n.owned) RETURN n" | |
puts "" | |
puts "Find all nodes in wave $num:" | |
puts "MATCH (n)-[r]->(m) WHERE n.wave=$num AND m.wave=$num RETURN n,r,m" | |
puts "" | |
puts "Show all waves up to and including wave $num:" | |
puts "MATCH (n)-[r]->(m) WHERE n.wave<=$num RETURN n,r,m" | |
puts "" | |
puts "Set owned and wave properties for a node (named $name, compromised via $method in wave $num):" | |
puts "MATCH (n) WHERE (n.name = '$name') SET n.owned = '$method', n.wave = $num" | |
puts "" | |
puts "Find spread of compromise for owned nodes in wave $num:" | |
puts "OPTIONAL MATCH (n1:User {wave:$num}) WITH collect(distinct n1) as c1 OPTIONAL MATCH (n2:Computer {wave:$num}) WITH collect(distinct n2) + c1 as c2 UNWIND c2 as n OPTIONAL MATCH p=shortestPath((n)-[*..20]->(m)) WHERE not(exists(m.wave)) WITH DISTINCT(m) SET m.wave=$num" | |
puts "" | |
puts "Show clusters of password reuse:" | |
puts "MATCH p=(n)-[r:SharesPasswordWith]->(m) RETURN p" | |
puts "" | |
puts "Show blacklisted nodes:" | |
puts "MATCH (n) WHERE exists(n.blacklist) RETURN n" | |
puts "" | |
puts "Show blacklisted relationships:" | |
puts "MATCH (n)-[r]->(m) WHERE exists(r.blacklist) RETURN n,r,m" | |
exit | |
end | |
def craft(options) | |
# get names of all nodes | |
hash = Hash.new { |h,k| h[k] = [] } | |
if options.nodes | |
hash['statements'] << {'statement' => "MATCH (n) RETURN (n.name)"} | |
return hash.to_json | |
# remove owned/wave/blacklist properties, delete SharesPasswordWith relationships | |
elsif options.reset | |
puts blue("[*]") + " Removing all custom properties and custom relationships" | |
hash['statements'] << {'statement' => "MATCH (n) WHERE exists(n.wave) OR exists(n.owned) OR exists(n.blacklist) REMOVE n.wave, n.owned, n.blacklist"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:SharesPasswordWith]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r {blacklist:true}]-(m) REMOVE r.blacklist"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_22]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_80]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_135]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_139]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_389]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_443]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_445]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_1433]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_1521]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_3306]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_3389]-(m) DELETE r"} | |
hash['statements'] << {'statement' => "MATCH (n)-[r:Connected_5432]-(m) DELETE r"} | |
return hash.to_json | |
# once nodes are added, set 'wave' for newly owned nodes | |
elsif options.spread | |
hash['statements'] << {'statement' => "OPTIONAL MATCH (n1:User {wave:#{options.wave}}) WITH collect(distinct n1) as c1 OPTIONAL MATCH (n2:Computer {wave:#{options.wave}}) WITH collect(distinct n2) + c1 as c2 UNWIND c2 as n OPTIONAL MATCH p=shortestPath((n)-[*..20]->(m)) WHERE not(exists(m.wave)) WITH DISTINCT(m) SET m.wave=#{options.wave} RETURN m.name, #{options.wave}", 'includeStats' => true} | |
return hash.to_json | |
# add 'owned' and 'wave' properties to nodes from file | |
elsif options.add | |
File.foreach(options.add) do |node| | |
name, method = node.split(',', 2) | |
if method.nil? | |
method = "Not specified" | |
end | |
# if -w flag set, then overwrite previous property if it exists, otherwise don't overwrite | |
if options.forceWave.nil? | |
hash['statements'] << {'statement' => "MATCH (n) WHERE (n.name = \"#{name.chomp}\") SET n.owned = \"#{method.chomp}\", n.wave = #{options.wave} RETURN \'#{name.chomp}\', \'#{options.wave}\', \'#{method.chomp}\'", 'includeStats' => true} | |
else | |
# this uses a Cypher hack for doing if/else conditionals | |
hash['statements'] << {'statement' => "MATCH (n) WHERE (n.name = \"#{name.chomp}\") FOREACH (ignoreMe in CASE WHEN exists(n.wave) THEN [1] ELSE [] END | SET n.wave=n.wave) FOREACH (ignoreMe in CASE WHEN not(exists(n.wave)) THEN [1] ELSE [] END | SET n.owned = \"#{method.chomp}\", n.wave = #{options.wave}) RETURN \'#{name.chomp}\',\'#{options.wave}\',\'#{method.chomp}\'", 'includeStats' => true} | |
end | |
end | |
return hash.to_json | |
# add 'owned' property to nodes from file | |
elsif options.addnowave | |
File.foreach(options.addnowave) do |node| | |
name, method = node.split(',', 2) | |
if method.nil? | |
method = "Not specified" | |
end | |
hash['statements'] << {'statement' => "MATCH (n) WHERE (n.name = \"#{name.chomp}\") SET n.owned = \"#{method.chomp}\" RETURN \'#{name.chomp}\', \'#{method.chomp}\'", 'includeStats' => true} | |
end | |
return hash.to_json | |
# Create SharesPasswordWith relationships between all nodes in file | |
elsif options.spw | |
nodes = [] | |
File.foreach(options.spw) do |node| | |
nodes.push(node) | |
end | |
nodes.combination(2).to_a.each do |n,m| | |
hash['statements'] << {'statement' => "MATCH (n {name:\"#{n.chomp}\"}),(m {name:\"#{m.chomp}\"}) WITH n,m MERGE (n)-[:SharesPasswordWith]->(m) WITH n,m MERGE (n)<-[:SharesPasswordWith]-(m) RETURN \'#{n.chomp}\',\'#{m.chomp}\'", 'includeStats' => true} | |
end | |
return hash.to_json | |
# add 'blacklist' property to each node | |
elsif options.blacklistn | |
File.foreach(options.blacklistn) do |node| | |
hash['statements'] << {'statement' => "MATCH (n {name:\"#{node.chomp}\"}) SET n.blacklist = true RETURN \'#{node.chomp}\'", 'includeStats' => true} | |
end | |
return hash.to_json | |
# add 'blacklist' property to each relationship | |
elsif options.blacklistr | |
File.foreach(options.blacklistr) do |path| | |
first, rel, last = path.split(',', 3) | |
hash['statements'] << {'statement' => "MATCH (n {name:\"#{first.chomp}\"})-[r:#{rel.chomp}]->(m {name:\"#{last.chomp}\"}) SET r.blacklist = true RETURN \'#{first.chomp}\',\'#{rel.chomp}\',\'#{last.chomp}\' ", 'includeStats' => true} | |
end | |
return hash.to_json | |
# remove 'blacklist' property from each node | |
elsif options.rblacklistn | |
File.foreach(options.rblacklistn) do |node| | |
puts blue("[*]") + " Removing blacklist property from \'#{node.chomp}\'" | |
hash['statements'] << {'statement' => "MATCH (n {name:\"#{node.chomp}\"}) REMOVE n.blacklist RETURN \'#{node.chomp}\'", 'includeStats' => true} | |
end | |
return hash.to_json | |
# remove 'blacklist' property from each relationship | |
elsif options.rblacklistr | |
File.foreach(options.rblacklistr) do |path| | |
first, rel, last = path.split(',', 3) | |
puts blue("[*]") + " Removing blacklist property from relationship \'#{rel.chomp}\' between \'#{first.chomp}\' and \'#{last.chomp}\'" | |
hash['statements'] << {'statement' => "MATCH (n {name:\"#{first.chomp}\"})-[r:#{rel.chomp}]->(m {name:\"#{last.chomp}\"}) REMOVE r.blacklist RETURN \'#{first.chomp}\',\'#{rel.chomp}\',\'#{last.chomp}\' ", 'includeStats' => true} | |
end | |
return hash.to_json | |
# add connection | |
elsif options.connection | |
# initial list of interesting ports | |
ports = ["22","80","135","139","389","443","445","1433","1521","3306","3389","5432"] | |
edges = Set.new | |
nodes = {} | |
File.foreach(options.connection) do |conn| | |
conn.chomp! | |
fields = conn.split() | |
(src, sport) = fields[1].split(/:/) | |
(dst, dport) = fields[2].split(/:/) | |
# assumption: ports indicated the destination port | |
if ports.member? sport | |
edges << [dst, src, sport] | |
elsif ports.member? dport | |
edges << [src, dst, dport] | |
end | |
end | |
dns = [] | |
if options.dns | |
dns = File.readlines(options.dns) | |
end | |
edges.each do |edge| | |
sname = "" | |
dname = "" | |
# if source IP is in DNS, get Computer name | |
if options.dns and dns.select{ |x| x.match(edge[0]) }.length > 0 | |
record = dns.select{ |x| x.match(edge[0]) }.last.split("\"") | |
sname = record[1] | |
# otherwise use IP as node name | |
else | |
sname = edge[0] | |
end | |
# if dest IP is in DNS, get Computer name | |
if options.dns and dns.select{ |x| x.match(edge[1]) }.length > 0 | |
record = dns.select{ |x| x.match(edge[1]) }.last.split("\"") | |
dname = record[1] | |
# otherwise use IP as node name | |
else | |
dname = edge[1] | |
end | |
hash['statements'] << {'statement' => "MERGE (s:Computer {name:\"#{sname}\"}) MERGE (d:Computer {name:\"#{dname}\"}) MERGE (s)-[:Connected_#{edge[2]}]->(d)", 'includeStats' => true} | |
end | |
return hash.to_json | |
end | |
end | |
def sendrequest(options) | |
uri = URI.parse(options.url) | |
# Create the HTTP object | |
http = Net::HTTP.new(uri.host, uri.port) | |
http.read_timeout = 120 | |
request = Net::HTTP::Post.new(uri.request_uri) | |
request["Accept"] = "application/json; charset=UTF-8" | |
request.content_type = "application/json" | |
request.basic_auth options.username, options.password | |
if options.wave == -1 | |
hash = Hash.new { |h,k| h[k] = [] } | |
hash['statements'] << {'statement' => "MATCH (n) WHERE exists(n.owned) RETURN max(n.wave)"} | |
request.body = hash.to_json | |
else | |
request.body = craft(options) | |
end | |
# Send the request | |
response = http.request(request) | |
parse(options, response) | |
end | |
def parse(options, response) | |
# | |
# print all nodes | |
# | |
if options.nodes | |
out = [] | |
data = JSON.parse(response.body) | |
# cycle through results (should only be 1) | |
data['results'].each do |r| | |
# cycle through rows returned | |
r['data'].each do |d| | |
# add to array | |
out.push(d['row']) | |
end if r['data'].any? | |
end if data['results'].any? | |
# sort, uniq, display | |
puts out.sort.uniq | |
# | |
# determine wave number | |
# | |
elsif options.wave == -1 | |
resp = JSON.parse(response.body) | |
resp['results'].each do |r| | |
r['data'].each do |d| | |
if d['row'] == [nil] | |
puts blue("[*]") + " No previously owned nodes found, setting wave to 1" | |
options.wave = 1 | |
else | |
options.wave = d['row'][0].to_i + 1 | |
end | |
end if r['data'].any? | |
end if resp['results'].any? | |
# | |
# parse spread of compromise | |
# | |
elsif options.spread | |
resp = JSON.parse(response.body) | |
resp['results'].each do |r| | |
puts blue("[*]") + " Finding spread of compromise for wave #{options.wave}" | |
r['stats'].each do |s| | |
# check stats to see if properties were set | |
if s.first == "properties_set" and s.last == 0 | |
# if there are records in data, then properties already set | |
if r['data'].any? | |
r['data'].each do |d| | |
puts blue("[*]") + " No additional nodes found for wave #{options.wave}" | |
end | |
else | |
puts red("[-]") + " No additional nodes found for wave #{options.wave}" | |
end | |
elsif s.first == "properties_set" and s.last != 0 | |
out = [] | |
count = 0 | |
# cycle through rows returned | |
r['data'].each do |d| | |
next unless not d['row'][0].to_s.empty? | |
out.push(d['row'][0]) | |
count += 1 | |
end if r['data'].any? | |
puts green("[+]") + " #{count} nodes found:" | |
puts out.sort.uniq | |
end | |
end if r['stats'].any? | |
end if resp['results'].any? | |
# | |
# parse nodes added | |
# | |
elsif options.add | |
success = true | |
resp = JSON.parse(response.body) | |
resp['results'].each do |r| | |
# node names provided are returned as columns | |
names = [] | |
r['columns'].each do |c| | |
names.push(c) | |
end | |
r['stats'].each do |s| | |
# check stats to see if properties were set | |
if s.first == "properties_set" and s.last == 0 | |
# if there are records in data, then properties already set | |
if not r['data'].any? | |
puts red("[-]") + " Properties not added for #{names.first} (node not found, check spelling?)" | |
success = false | |
end | |
elsif s.first == "properties_set" and s.last == 2 | |
puts green("[+]") + " Success, marked #{names.first} as owned in wave #{names[1]} via #{names.last}" | |
elsif s.first == "properties_set" and s.last == 1 | |
puts blue("[*]") + " Properties already exist for #{names.first}, skipping (overwrite with flag -w <num>)" | |
end | |
end if r['stats'].any? | |
end if resp['results'].any? | |
# if all nodes were added successfully or skipped, find spread for new nodes | |
if success | |
options.spread = true | |
sendrequest(options) | |
else | |
puts red("[-]") + " Skipping finding spread of compromise due to \"node not found\" error" | |
end | |
# | |
# parse nodes added (no wave) | |
# | |
elsif options.addnowave | |
resp = JSON.parse(response.body) | |
resp['results'].each do |r| | |
# node names provided are returned as columns | |
names = [] | |
r['columns'].each do |c| | |
names.push(c) | |
end | |
r['stats'].each do |s| | |
# check stats to see if properties were set | |
if s.first == "properties_set" and s.last == 0 | |
# if there are records in data, then properties already set | |
if not r['data'].any? | |
puts red("[-]") + " owned property not added for #{names.first} (node not found, check spelling?)" | |
end | |
elsif s.first == "properties_set" and s.last == 1 | |
puts green("[+]") + " Success, marked #{names.first} as owned via #{names.last}" | |
end | |
end if r['stats'].any? | |
end if resp['results'].any? | |
# | |
# parse blacklist node added | |
# | |
elsif options.blacklistn | |
resp = JSON.parse(response.body) | |
resp['results'].each do |r| | |
# node names provided are returned as columns | |
names = [] | |
r['columns'].each do |c| | |
names.push(c) | |
end | |
r['stats'].each do |s| | |
# check stats to see if properties were set | |
if s.first == "properties_set" and s.last == 0 | |
# if there are records in data, then properties already set | |
if not r['data'].any? | |
puts red("[-]") + " Property not added for #{names.first} (node not found, check spelling?)" | |
end | |
elsif s.first == "properties_set" and s.last == 1 | |
puts green("[+]") + " Success, marked #{names.first} as blacklisted" | |
#elsif s.first == "properties_set" and s.last == 1 | |
# puts blue("[*]") + " Property already exist for #{names.first}, skipping" | |
end | |
end if r['stats'].any? | |
end if resp['results'].any? | |
# | |
# parse blacklist rel added | |
# | |
elsif options.blacklistr | |
resp = JSON.parse(response.body) | |
resp['results'].each do |r| | |
# node names provided are returned as columns | |
names = [] | |
r['columns'].each do |c| | |
names.push(c) | |
end | |
r['stats'].each do |s| | |
# check stats to see if properties were set | |
if s.first == "properties_set" and s.last == 0 | |
# if there are records in data, then properties already set | |
if not r['data'].any? | |
puts red("[-]") + " Property not added for (#{names.first})-[:#{names[1]}]->(#{names.last}), (node or relationship not found, check spelling?)" | |
end | |
elsif s.first == "properties_set" and s.last == 1 | |
puts green("[+]") + " Success, marked #{names[1]} as blacklisted from #{names.first} to #{names.last}" | |
#elsif s.first == "properties_set" and s.last == 1 | |
# puts blue("[*]") + " Property already exist for #{names.first}, skipping" | |
end | |
end if r['stats'].any? | |
end if resp['results'].any? | |
# | |
# parse SharesPasswordWith | |
# | |
elsif options.spw | |
resp = JSON.parse(response.body) | |
resp['results'].each do |r| | |
# node names provided are returned as columns | |
names = [] | |
r['columns'].each do |c| | |
names.push(c) | |
end | |
r['stats'].each do |s| | |
# check stats to see if a relationship was created | |
if s.first == "relationships_created" and s.last == 0 | |
# if there are records in data, then relationship already exists | |
if r['data'].any? | |
r['data'].each do |d| | |
puts blue("[*]") + " Relationship already exists for #{names.first} and #{names.last}" | |
end | |
else | |
puts red("[-]") + " Relationship not created for #{names.first} and #{names.last} (check spelling)" | |
end | |
elsif s.first == "relationships_created" and s.last == 2 | |
puts green("[+]") + " Created SharesPasswordWith relationship between #{names.first} and #{names.last}" | |
elsif s.first == "relationships_created" and (s.last != 0 or s.last != 2) | |
puts "Something went wrong when creating SharesPasswordWith relationship" | |
end | |
end if r['stats'].any? | |
end if resp['results'].any? | |
end | |
# uncomment line below to debug | |
#puts JSON.pretty_generate(JSON.parse(response.body)) | |
end | |
def main() | |
options = OpenStruct.new | |
ARGV << '-h' if ARGV.empty? | |
OptionParser.new do |opt| | |
opt.banner = "Usage: ruby bh-owned.rb [options]" | |
opt.on('Server Details:') | |
opt.on('-u', '--username <username>', 'Neo4j database username (default: \'neo4j\')') { |o| options.username = o } | |
opt.on('-p', '--password <password>', 'Neo4j database password (default: \'BloodHound\')') { |o| options.password = o } | |
opt.on('-U', '--url <url>', 'URL of Neo4j RESTful host (default: \'http://127.0.0.1:7474/\')') { |o| options.url = o } | |
opt.on('Owned/Wave/SPW:') | |
opt.on('-a', '--add <file>', 'add \'owned\' and \'wave\' property to nodes in <file>') { |o| options.add = o } | |
opt.on('-A', '--add-no-wave <file>', 'add \'owned\' property to nodes in <file> (skip \'wave\' property)') { |o| options.addnowave = o } | |
opt.on('-w', '--wave <num>', Integer, 'value to set \'wave\' property (override default behavior)') { |o| options.wave = o } | |
opt.on('-s', '--spw <file>', 'add \'SharesPasswordWith\' relationship between all nodes in <file>') { |o| options.spw = o } | |
opt.on('Blacklisting:') | |
opt.on('-b', '--bl-node <file>', 'add \'blacklist\' property to nodes in <file>') { |o| options.blacklistn = o } | |
opt.on('-B', '--bl-rel <file>', 'add \'blacklist\' property to relationships in <file>') { |o| options.blacklistr = o } | |
opt.on('-r', '--remove-bl-node <file>', 'remove \'blacklist\' property from nodes in <file>') { |o| options.rblacklistn = o } | |
opt.on('-R', '--remove-bl-rel <file>', 'remove \'blacklist\' property from relationships in <file>') { |o| options.rblacklistr = o } | |
opt.on('Connections:') | |
opt.on('-c', '--connections <file>', 'add connection info from netstat <file>') { |o| options.connection = o } | |
opt.on('-d', '--dns <file>', 'contains DNS mapping of IP to computer name (10.2.3.4,srv1.int.local)') { |o| options.dns = o } | |
opt.on('Misc Queries:') | |
opt.on('-n', '--nodes', 'get all node names') { |o| options.nodes = o } | |
opt.on('-e', '--examples', 'reference doc of custom Cypher queries for BloodHound') { |o| options.examples = o } | |
opt.on('--reset', 'remove all custom properties and SharesPasswordWith relationships') { |o| options.reset = o } | |
end.parse! | |
if options.examples | |
examples() | |
end | |
if options.username.nil? | |
options.username = 'neo4j' | |
puts blue("[*]") + " Using default username: neo4j" | |
end | |
if options.password.nil? | |
options.password = 'BloodHound' | |
puts blue("[*]") + " Using default password: BloodHound" | |
end | |
if options.url.nil? | |
options.url = 'http://127.0.0.1:7474/db/data/transaction/commit' | |
puts blue("[*]") + " Using default URL: http://127.0.0.1:7474/" | |
else | |
options.url = options.url.gsub(/\/+$/, '') + '/db/data/transaction/commit' | |
puts blue("[*]") + " URL set: #{options.url}" | |
end | |
if options.add | |
if File.exist?(options.add) == false | |
puts red("#{options.add} does not exist! Exiting.") | |
exit 1 | |
elsif options.addnowave.nil? | |
if options.wave.nil? | |
# -1 means we don't know current max "n.wave" value | |
options.wave = -1 | |
sendrequest(options) | |
options.forceWave = true | |
end | |
else | |
# -2 means we're skipping wave/spread | |
options.wave = -2 | |
end | |
end | |
if options.spw | |
if File.exist?(options.spw) == false | |
puts red("#{options.spw} does not exist! Exiting.") | |
exit 1 | |
end | |
end | |
if options.blacklistn | |
if File.exist?(options.blacklistn) == false | |
puts red("#{options.blacklistn} does not exist! Exiting.") | |
exit 1 | |
end | |
end | |
if options.blacklistr | |
if File.exist?(options.blacklistr) == false | |
puts red("#{options.blacklistr} does not exist! Exiting.") | |
exit 1 | |
end | |
end | |
if options.rblacklistn | |
if File.exist?(options.rblacklistn) == false | |
puts red("#{options.rblacklistn} does not exist! Exiting.") | |
exit 1 | |
end | |
end | |
if options.rblacklistr | |
if File.exist?(options.rblacklistr) == false | |
puts red("#{options.rblacklistr} does not exist! Exiting.") | |
exit 1 | |
end | |
end | |
if options.connection | |
if File.exist?(options.connection) == false | |
puts red("#{options.connection} does not exist! Exiting.") | |
exit 1 | |
end | |
if options.dns | |
if File.exist?(options.dns) == false | |
puts red("#{options.dns} does not exist! Exiting.") | |
exit 1 | |
end | |
else | |
puts "No DNS file set (-d), using IP addresses as Computer node names." | |
end | |
end | |
options.spread = false | |
sendrequest(options) | |
end | |
main() |