capra 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5c63e457d5d29a1a32e717b9771a1a77c96c2f473e8e2724ccb19b7bcc1a9969
4
+ data.tar.gz: a2d6115618a11efcab273d51fd49a8c6c2f48b0debc5fe744d9f4242f20a38b9
5
+ SHA512:
6
+ metadata.gz: 5fc830199365d7aaa5164856c1c8b4accf135bc553ccbb6366055e9b8edccc4d56fded575a67c7424462ce6ec13c6031a18faba0a089e1756dadb0cf20fe4b97
7
+ data.tar.gz: f3bcb6939ca831543c5845206a729fab730d03fe395ce31ebd8359d4e74d26c78570a929c259eaaff4111d44422cefb9a96f0362958699f1dcc4250a84eaf005
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "capra"
5
+
6
+ Capra.run_cli!
@@ -0,0 +1,83 @@
1
+ require "packetgen"
2
+ require "ipaddr"
3
+ require "fileutils"
4
+ require "command_lion"
5
+ require "capra/version"
6
+ require "capra/private_ips"
7
+ require "capra/packetgen_extensions"
8
+ require "capra/snort_rule_parser"
9
+ require "capra/engine"
10
+ require "capra/version"
11
+
12
+ require "pry"
13
+
14
+ module Capra
15
+ class Error < StandardError; end
16
+
17
+ def self.run_cli!
18
+ CommandLion::App.run do
19
+ name "Capra"
20
+ version Capra::VERSION
21
+ description "Intrusion Detection System"
22
+
23
+ command :init do
24
+ description "create a base Caprafile in the current working directory"
25
+
26
+ action do
27
+ if File.exists?("Caprafile")
28
+ puts "error: Caprafile already exists!"
29
+ exit 1
30
+ end
31
+ File.open("Caprafile", 'w') do |file|
32
+ file.puts '#!/usr/bin/env ruby'
33
+ file.puts
34
+ file.puts "interface '#{Interfacez.default}'"
35
+ file.puts
36
+ file.puts "# your rules go here"
37
+ end
38
+ end
39
+ end
40
+
41
+ command :start do
42
+ description "start the engine"
43
+
44
+ default "Caprafile"
45
+
46
+ action do
47
+ unless File.exists?(argument)
48
+ puts "error: cannot find #{argument} in the current directory"
49
+ puts
50
+ puts "hint: run `capra init` to create a base Caprafile"
51
+ exit 1
52
+ end
53
+
54
+ Capra::Engine.new(file: argument)
55
+ end
56
+ end
57
+
58
+ # $ capra convert 'alert tcp any any -> any 21 (msg:"ftp")'
59
+ # rule 'TCP' do |packet|
60
+ # next unless packet.tcp.dport == 21
61
+ # alert "ftp"
62
+ # end
63
+ command :convert do
64
+ description "Convert Snort rule(s) to Caprafile syntax"
65
+
66
+ type :string
67
+
68
+ action do
69
+ if File.file?(argument)
70
+ File.foreach(argument) do |line|
71
+ line = line.strip
72
+ next if line.empty?
73
+
74
+ Capra::SnortRuleParser.convert(line)
75
+ end
76
+ else
77
+ Capra::SnortRuleParser.convert(argument)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,100 @@
1
+ module Capra
2
+ class Engine
3
+ attr_accessor :interface
4
+ attr_accessor :rules
5
+
6
+ def initialize(file: nil, &block)
7
+ default_interface
8
+ @rules = {}
9
+ if file
10
+ instance_eval File.read(file)
11
+ else
12
+ instance_eval &block
13
+ end
14
+ start!
15
+ end
16
+
17
+ def interface(iface)
18
+ @interface = iface
19
+ end
20
+
21
+ def default_interface
22
+ @interface = Interfacez.default
23
+ end
24
+
25
+ def pcap(file)
26
+ @pcap = file
27
+ end
28
+
29
+ def save_to(file)
30
+ @save_to = file
31
+ end
32
+
33
+ def debug!
34
+ binding.pry
35
+ end
36
+
37
+ def rule(type, description: nil, reference: nil, &block)
38
+ if @rules[type]
39
+ @rules[type] << block
40
+ else
41
+ @rules[type] = [block]
42
+ end
43
+ end
44
+
45
+ def alert(mesg)
46
+ puts mesg
47
+ end
48
+
49
+ def email(recpt)
50
+ puts "Sending email!"
51
+ end
52
+
53
+ def save(packet)
54
+ @save_to = "capra-save-"+Time.now.utc.to_s.split(" ").join("-")+".pcapng" if @save_to.nil?
55
+
56
+ pf = PacketGen::PcapNG::File.new
57
+ pf.array_to_file [packet]
58
+ pf.to_f(@save_to, append: true)
59
+ end
60
+
61
+ def start!
62
+ if @pcap
63
+ read_pcap_file(@pcap) do |packet|
64
+ @rules.each do |header, blocks|
65
+ if header == 'ANY' || packet.is?(header)
66
+ blocks.each do |block|
67
+ block.call(packet)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ else
73
+ PacketGen.capture(iface: @interface) do |packet|
74
+ @rules.each do |header, blocks|
75
+ if header == 'ANY' || packet.is?(header)
76
+ blocks.each do |block|
77
+ block.call(packet)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def read_pcap_file(filename)
88
+ PcapNG::File.new.read_packets(filename) do |packet|
89
+ yield packet
90
+ end
91
+ rescue StandardError => e
92
+ PCAPRUB::Pcap.open_offline(filename).each_packet do |packet|
93
+ next unless (packet = PacketGen.parse(packet.to_s))
94
+
95
+ yield packet
96
+ end
97
+ end
98
+ end
99
+ end
100
+
@@ -0,0 +1,149 @@
1
+ module PacketGen
2
+ class Packet
3
+ def ftp?
4
+ return false unless self.is? 'TCP'
5
+ self.tcp.dport == 21 || self.tcp.sport == 21
6
+ end
7
+
8
+ def ssh?
9
+ return false unless self.is? 'TCP'
10
+ self.tcp.dport == 22 || self.tcp.sport == 22
11
+ end
12
+
13
+ def icmp?
14
+ self.is? 'ICMP'
15
+ end
16
+
17
+ def http?
18
+ return false unless self.is? 'TCP'
19
+ self.is? 'HTTP::Request' or self.is? 'HTTP::Response'
20
+ end
21
+
22
+ def https?
23
+ return false unless self.is? 'TCP'
24
+ self.tcp.dport == 443 || self.tcp.sport == 443
25
+ end
26
+
27
+ def telnet?
28
+ return false unless self.is? 'TCP'
29
+ self.tcp.dport == 23 || self.tcp.sport == 23
30
+ end
31
+
32
+ def dns?
33
+ return true if self.is? 'DNS'
34
+ end
35
+
36
+ def ip?
37
+ return true if self.is? 'IP'
38
+ end
39
+
40
+ def arp?
41
+ return true if self.is? 'ARP'
42
+ end
43
+ end
44
+
45
+ module Header
46
+ class TCP
47
+ def port?(int)
48
+ self.dport == int || self.dport == int
49
+ end
50
+ end
51
+
52
+ class DNS
53
+ def queries
54
+ return [] unless self.query? || self.response?
55
+ packet.dns.qd.map { |q| q.name.chop! }
56
+ end
57
+
58
+ def responses
59
+ return {} unless self.response?
60
+ info = {}
61
+ packet.dns.an.map do |a|
62
+ name = a.name.chop!
63
+ if info[name]
64
+ info[name] << a.human_rdata
65
+ else
66
+ info[name] = [a.human_rdata]
67
+ end
68
+ end
69
+ info
70
+ end
71
+ end
72
+
73
+ class IP
74
+ def internal_communication_only?
75
+ PRIVATE_IPS.any? { |private_ip| private_ip.include?(self.src) } and PRIVATE_IPS.any? { |private_ip| private_ip.include?(self.dst) }
76
+ end
77
+
78
+ def external_communication?
79
+ !internal_communication_only?
80
+ end
81
+
82
+ def internal_destination?
83
+ PRIVATE_IPS.any? { |private_ip| private_ip.include?(self.dst) }
84
+ end
85
+
86
+ def external_destination?
87
+ !internal_destination?
88
+ end
89
+
90
+ def internal_source?
91
+ PRIVATE_IPS.any? { |private_ip| private_ip.include?(self.src) }
92
+ end
93
+
94
+ def external_source?
95
+ !internal_source?
96
+ end
97
+
98
+ def within_subnet?(cidr)
99
+ subnet = IPAddr.new(cidr)
100
+ subnet.include?(self.src) or subnet.include?(self.dst)
101
+ end
102
+
103
+ def from_subnet?(cidr)
104
+ subnet = IPAddr.new(cidr)
105
+ subnet.include?(self.src)
106
+ end
107
+
108
+ def from_subnets?(cidrs)
109
+ cidrs.map { IPAddr.new(cidr) }.include?(self.src)
110
+ end
111
+
112
+ def to_subnet?(cidr)
113
+ subnet = IPAddr.new(cidr)
114
+ subnet.include?(self.dst)
115
+ end
116
+
117
+ def to_subnets?(cidr)
118
+ cidrs.map { IPAddr.new(cidr) }.include?(self.dst)
119
+ end
120
+ end
121
+
122
+ class ICMP
123
+ def echo_reply?
124
+ self.type == 0
125
+ end
126
+
127
+ def destination_unreachable?
128
+ self.type == 3
129
+ end
130
+
131
+ def redirect?
132
+ self.type == 5
133
+ end
134
+
135
+ def echo?
136
+ self.type == 8
137
+ end
138
+
139
+ def router_advertisement?
140
+ self.type == 9
141
+ end
142
+
143
+ def router_solicitation?
144
+ self.type == 10
145
+ end
146
+ end
147
+ end
148
+ end
149
+
@@ -0,0 +1,7 @@
1
+ module Capra
2
+ PRIVATE_IPS = [
3
+ IPAddr.new('10.0.0.0/8'),
4
+ IPAddr.new('172.16.0.0/12'),
5
+ IPAddr.new('192.168.0.0/16'),
6
+ ].freeze
7
+ end
@@ -0,0 +1,117 @@
1
+ module Capra
2
+ module SnortRuleParser
3
+ def self.parse(rule)
4
+ # alert tcp $EXTERNAL_NET any -> $HOME_NET 21 (msg:"FTP MDTM overflow attempt"; flow:to_server,established; content:"MDTM"; nocase; isdataat:100,relative; pcre:"/^MDTM\s[^\n]{100}/smi"; reference:bugtraq,9751; reference:cve,2001-1021; reference:cve,2004-0330; reference:nessus,12080; classtype:attempted-admin; sid:2546; rev:5;)
5
+ rule_parts = rule.split
6
+
7
+ rule_options = {}
8
+
9
+ rule_parts[7..-1].join(" ").sub("(",'').sub(")",'').split(";").map { |opt| opt.split(":").map { |val| val.gsub('"', '') }}.each do |k, v|
10
+ k = k.strip
11
+
12
+ if rule_options[k]
13
+ rule_options[k] << v
14
+ else
15
+ rule_options[k] = [v]
16
+ end
17
+ end
18
+
19
+ {
20
+ action: rule_parts[0],
21
+ protocol: rule_parts[1],
22
+ source_ip: rule_parts[2],
23
+ source_port: rule_parts[3],
24
+ direction: rule_parts[4], # almost always -> unless you're crazy?
25
+ destination_ip: rule_parts[5],
26
+ destination_port: rule_parts[6],
27
+ options: rule_options
28
+ }
29
+ end
30
+
31
+ def self.convert(rule)
32
+ parsed_rule = self.parse(rule)
33
+ puts "rule '#{parsed_rule[:protocol].upcase}' do |packet|"
34
+ unless parsed_rule[:source_ip] == "any"
35
+ if parsed_rule[:source_ip] == "$EXTERNAL_NET" # might want to check direction too?
36
+ puts "\tnext unless packet.ip.external_source?"
37
+ elsif parsed_rule[:source_ip][0] == "["
38
+ puts "\tnext unless packet.ip.from_subnets?(#{parsed_rule[:source_ip].sub("[","").sub("]","").split(",").inspect})"
39
+ else
40
+ puts "\tnext unless packet.ip.src == '#{parsed_rule[:source_ip]}'"
41
+ end
42
+ end
43
+ unless parsed_rule[:source_port] == "any"
44
+ puts "\tnext unless packet.#{parsed_rule[:protocol]}.sport == #{parsed_rule[:source_port]}"
45
+ end
46
+ unless parsed_rule[:destination_ip] == "any"
47
+ if parsed_rule[:destination_ip] == "$HOME_NET"
48
+ puts "\tnext unless packet.ip.internal_destination?"
49
+ elsif parsed_rule[:source_ip][0] == "["
50
+ parsed_rule[:source_ip].sub("[","").sub("]","").split(",").each do |cidr|
51
+ puts "\tnext unless packet.ip.to_subnets?(#{cidr})"
52
+ end
53
+ else
54
+ puts "\tnext unless packet.ip.dst == '#{parsed_rule[:destination_ip]}'"
55
+ end
56
+ end
57
+ unless parsed_rule[:destination_port] == "any"
58
+ puts "\tnext unless packet.#{parsed_rule[:protocol]}.dport == #{parsed_rule[:destination_port]}"
59
+ end
60
+ # TODO: need to support mixed string and byte matching, even though it's insane
61
+ if parsed_rule[:options]["content"]
62
+ parsed_rule[:options]["content"].each do |content|
63
+ if content[0] == "|" and content[-1] == "|"
64
+ puts "\tnext packet.body.unpack('H*').first.include?(\"#{content.gsub("|","").split.join}\")"
65
+ else
66
+ puts "\tnext unless packet.body.include?(\"#{content}\")"
67
+ end
68
+ end
69
+ #puts "\tnext unless packet.body.include?(\"#{parsed_rule[:options]["content"]}\")"
70
+ end
71
+ if parsed_rule[:options]["pcre"]
72
+ parsed_rule[:options]["pcre"].each do |pcre|
73
+ regex, regex_ops = pcre.split("/")[1..-1]
74
+ regex_ops_value = regex_ops.split('').map do |str|
75
+ case str
76
+ when "i"
77
+ Regexp::IGNORECASE
78
+ when "m"
79
+ Regexp::MULTILINE
80
+ when "x"
81
+ Regexp::EXTENDED
82
+ else
83
+ 0
84
+ end
85
+ end.sum
86
+ puts "\tnext unless packet.body.match(Regexp.new('#{regex}', #{regex_ops_value}))"
87
+ end
88
+ end
89
+ if parsed_rule[:options]['flags']
90
+ parsed_rule[:options]['flags'].each do |flag_opt|
91
+ flag_opt.split('').each do |flag|
92
+ case flag
93
+ when 'F' #fin
94
+ puts "\tnext unless packet.tcp.flag_fin?"
95
+ when 'S' #syn
96
+ puts "\tnext unless packet.tcp.flag_syn?"
97
+ when 'R' #rst
98
+ puts "\tnext unless packet.tcp.flag_rst?"
99
+ when 'P' #psh
100
+ puts "\tnext unless packet.tcp.flag_psh?"
101
+ when 'A' #ack
102
+ puts "\tnext unless packet.tcp.flag_ack?"
103
+ when 'U' #urg
104
+ puts "\tnext unless packet.tcp.flag_urg?"
105
+ end
106
+ end
107
+ end
108
+ end
109
+ # alert probably needs to be the last thing in the rule for it to work properly in this case
110
+ if parsed_rule[:options]["msg"]
111
+ puts "\talert \"#{parsed_rule[:options]["msg"].first}\""
112
+ end
113
+ puts "end"
114
+ end
115
+ end
116
+ end
117
+
@@ -0,0 +1,3 @@
1
+ module Capra
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: capra
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Kent 'picat' Gruber
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-04-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: packetgen
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.1.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.1.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: oj
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.7.11
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.7.11
41
+ - !ruby/object:Gem::Dependency
42
+ name: command_lion
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.0.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.0.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: ipaddr
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.2.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.2.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.17'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.17'
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: 3.8.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 3.8.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.12.2
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.12.2
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry-coolline
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.2.5
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.2.5
139
+ description:
140
+ email:
141
+ - kgruber1@emich.edu
142
+ executables:
143
+ - capra
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - bin/capra
148
+ - lib/capra.rb
149
+ - lib/capra/engine.rb
150
+ - lib/capra/packetgen_extensions.rb
151
+ - lib/capra/private_ips.rb
152
+ - lib/capra/snort_rule_parser.rb
153
+ - lib/capra/version.rb
154
+ homepage: https://github.com/picatz/capra
155
+ licenses:
156
+ - MIT
157
+ metadata: {}
158
+ post_install_message: |-
159
+ Thank you for installing Capra!
160
+ Make sure to install `libpcap-dev` if you haven't already!
161
+ rdoc_options: []
162
+ require_paths:
163
+ - lib
164
+ required_ruby_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ required_rubygems_version: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ requirements:
175
+ - libpcap-dev
176
+ rubyforge_project:
177
+ rubygems_version: 3.0.0.beta1
178
+ signing_key:
179
+ specification_version: 4
180
+ summary: Intrusion detection system.
181
+ test_files: []