aoandon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ # Set default behaviour, in case users don't have core.autocrlf set.
2
+ * text=auto
3
+
4
+ # Explicitly declare text files we want to always be normalized and converted
5
+ # to native line endings on checkout.
6
+ *.rb text
7
+
8
+ # Denote all files that are truly binary and should not be modified.
9
+ *.png binary
10
+ *.jpg binary
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .DS_Store
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ log/*
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
@@ -0,0 +1 @@
1
+ 1.9.3-p194
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'pcap', '~> 0.7.0'
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Cyril Wack
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.
@@ -0,0 +1,296 @@
1
+ # Aoandon
2
+
3
+ Aoandon (青行燈) is a minimalist network intrusion detection system (NIDS).
4
+
5
+ ![Blue andon creature](https://raw.github.com/cyril/aoandon/master/blue-andon-creature.jpg)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'aoandon'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install aoandon
20
+
21
+ ## Usage
22
+
23
+ Aoandon NIDS is the selective ignoring or alerting of data packets as they pass through its network interface. The criteria that it uses when inspecting packets are based on the Layer 3 (IPv4 and IPv6) and Layer 4 (TCP, UDP) headers. The most often used criteria are source and destination address, source and destination port, and protocol.
24
+
25
+ Rules specify the criteria that a packet must match and the resulting action, either pass or alert, that is taken when a match is found. Rules are evaluated in sequential order, first to last. Unless the packet matches a rule containing the `quick` keyword, the packet will be evaluated against all rules before the final action is taken. The last rule to match is the *winner* and will dictate what action to take on the packet. There is an implicit pass all at the beginning of a ruleset meaning that if a packet does not match any rule the resulting action will be pass.
26
+
27
+ Both static and dynamic ruleset can be applied to packets.
28
+
29
+ ### Static ruleset
30
+
31
+ Aoandon NIDS reads its configuration rules from `config/rules.yml` at boot time. In order to be able to load rules, this JSON/YAML file must have at least a `rules` key.
32
+
33
+ #### Rule syntax
34
+
35
+ The general syntax for static rules is:
36
+
37
+ 1. action
38
+ 2. context
39
+ 3. options
40
+
41
+ Where *action* can use as a logger level such as INFO or ERROR that indicate alerts' importance. Note: the `pass` action will ignore the packet back to the kernel for further processing while any other action will react.
42
+
43
+ Every *context* params are evaluated for analysis to determine whether a given package matches.
44
+
45
+ The last part, *options*, can be:
46
+
47
+ * `log`: specifies that the packet should be logged.
48
+ * `quick`: if a packet matches a rule specifying `quick`, then that rule is considered the last matching rule and the specified action is taken.
49
+ * `msg`: tells the alerting engine the message to print to an alert.
50
+
51
+ #### Default alert
52
+
53
+ The recommended practice when setting up a NIDS is to take a "default alert" approach. That is, to alert everything and then selectively allow certain traffic through the interface. This approach is recommended because it errs on the side of caution and also makes writing a ruleset easier.
54
+
55
+ To create a default alert sniffer policy, the first rules should be:
56
+
57
+ ```yaml
58
+ [ info, {}, {log: true, msg: "Suspected packet!"} ]
59
+ ```
60
+
61
+ This will alert all traffic on the given interface in either direction from anywhere to anywhere.
62
+
63
+ #### The `quick` keyword
64
+
65
+ As indicated earlier, each packet is evaluated against the sniffer ruleset from top to bottom. By default, the packet is marked for passage, which can be changed by any rule, and could be changed back and forth several times before the end of the sniffer rules. The last matching rule *wins*. There is an exception to this: the `quick` option on a sniffing rule has the effect of canceling any further rule processing and causes the specified action to be taken. Let's look at a couple examples:
66
+
67
+ Wrong:
68
+
69
+ ```yaml
70
+ - [ crit, {proto: tcp, to: {port: 22}}, {msg: "...SSH?", log: true} ]
71
+ - [ pass, {} ]
72
+ ```
73
+
74
+ In this case, the alert line may be evaluated, but will never have any effect, as it is then followed by a line which will ignore everything.
75
+
76
+ Better:
77
+
78
+ ```yaml
79
+ - [ crit, {proto: tcp, to: {port: 22}}, {msg: "...SSH?", log: true, quick: true} ]
80
+ - [ pass, {} ]
81
+ ```
82
+
83
+ These rules are evaluated a little differently. If the alert line is matched, due to the `quick` option, the packet will be reported, and the rest of the ruleset will be ignored.
84
+
85
+ #### Ruleset example
86
+
87
+ ```yaml
88
+ hosts:
89
+ - &honeypots [ 192.168.1.4, 192.168.1.9 ]
90
+ - &my_station 192.168.1.38
91
+
92
+ rules:
93
+ # "default alert" approach
94
+ - [ info, {}, {log: true, msg: "Suspected packet!"} ]
95
+
96
+ # then, selectively ignore certain traffic
97
+ - [ warn, {to: {addr: *honeypots}}, {msg: "Touché.", quick: true, log: true} ]
98
+ - [ pass, {from: {addr: *my_station}} ]
99
+ - [ pass, {to: {addr: *my_station}} ]
100
+ - [ pass, {to: {addr: '224.0.0.1'}} ]
101
+ ```
102
+
103
+ #### A more complete ruleset example
104
+
105
+ ```yaml
106
+ macros:
107
+ web_server: &web_server
108
+ 114.21.70.71
109
+ gateway: &gw
110
+ 192.168.0.1
111
+
112
+ tables:
113
+ redzone: &redzone
114
+ - "81.15.142.23"
115
+ hacker: &id001
116
+ - 81.15.142.23
117
+ - 42.154.25.213
118
+ blacklist: &blacklist
119
+ - *id001
120
+ - *gw
121
+ - 81.15.142.23
122
+ - "64.81.240.57"
123
+ unknown:
124
+ - any
125
+ mz: &mz
126
+ 192.168.0.201
127
+ dmz: &dmz
128
+ sql_server: &sql_server
129
+ 10.0.0.2
130
+
131
+ ports:
132
+ web: &www
133
+ - 80
134
+ - 443
135
+ p2p:
136
+ - 63192
137
+
138
+ messages:
139
+ - &msg001 "ICMP packet from Google to MZ"
140
+ - &msg002 "MZ intrusion detected!"
141
+
142
+ rules:
143
+ # "default alert" approach
144
+ - [ info, {}, {quick: true, log: true, msg: "Suspected packet!"} ]
145
+
146
+ # then, selectively ignore certain traffic
147
+ - [ pass, {af: inet, from: {addr: any}, to: {addr: any}} ]
148
+ - [ warn, {proto: tcp, from: {addr: *blacklist}, to: {addr: any, port: *www}, flags: syn} ]
149
+ - [ warn, {proto: tcp, from: {addr: any, port: 123}, to: {addr: *dmz}} ]
150
+ - [ crit, {af: inet6, from: {addr: any}, to: {addr: any}}, {log: true} ]
151
+ - [ pass, {af: inet, proto: tcp, from: {addr: *mz}, to: {addr: *web_server, port: *www}, {quick: true}} ]
152
+ - [ warn, {proto: udp, from: {addr: *redzone}, to: {addr: 10.1.0.32, port: 21}} ]
153
+ - [ info, {proto: tcp, from: {addr: 172.16.0.6}, to: {addr: 192.168.0.14, port: 22}} ]
154
+ - [ crit, {proto: tcp, from: {addr: *blacklist}, to: {addr: *mz}}, {log: true, msg: *msg002} ]
155
+ - [ info, {proto: tcp, to: {addr: 192.168.0.14, port: 22}} ]
156
+ - [ pass, {proto: tcp, from: {addr: *id001}, to: {addr: *sql_server, port: 3306}} ]
157
+ - [ info, {af: inet, proto: icmp, from: {addr: google.com}, to: {addr: *mz}}, {log: true, msg: *msg001} ]
158
+ ```
159
+
160
+ ### Dynamic ruleset
161
+
162
+ Some semantic analysis can also be done through Aoandon NIDS extensions, using modules such as:
163
+
164
+ ```ruby
165
+ # lib/aoandon/dynamic_rule/less1024.rb
166
+ module Aoandon
167
+ module DynamicRule
168
+ module Less1024
169
+ MESSAGE = 'Port numbers < 1024'
170
+ PROTO_TCP = 6
171
+ PROTO_UDP = 17
172
+ WELL_KNOWN_PORTS = (0..1023)
173
+
174
+ def self.control?(packet)
175
+ (tcp?(packet) || (udp?(packet) && different_ports?(packet.sport, packet.dport))) &&
176
+ less_1024?(packet.sport) && less_1024?(packet.dport)
177
+ end
178
+
179
+ def self.logging?(packet)
180
+ false
181
+ end
182
+
183
+ private
184
+
185
+ def self.different_ports?(src_port, dst_port)
186
+ src_port != dst_port
187
+ end
188
+
189
+ def self.less_1024?(port)
190
+ WELL_KNOWN_PORTS.include?(port)
191
+ end
192
+
193
+ def self.tcp?(packet)
194
+ packet.ip_proto == PROTO_TCP
195
+ end
196
+
197
+ def self.udp?(packet)
198
+ packet.ip_proto == PROTO_UDP
199
+ end
200
+ end
201
+ end
202
+ end
203
+ ```
204
+
205
+ ```ruby
206
+ # lib/aoandon/dynamic_rule/more_fragments.rb
207
+ module Aoandon
208
+ module DynamicRule
209
+ module MoreFragments
210
+ MESSAGE = 'More Fragment bit is set'
211
+
212
+ def self.control?(packet)
213
+ packet.ip_mf?
214
+ end
215
+
216
+ def self.logging?(packet)
217
+ false
218
+ end
219
+ end
220
+ end
221
+ end
222
+ ```
223
+
224
+ ```ruby
225
+ # lib/aoandon/dynamic_rule/same_ip.rb
226
+ module Aoandon
227
+ module DynamicRule
228
+ module SameIp
229
+ LOCALHOST = '127.0.0.1'
230
+ MESSAGE = 'Same IP'
231
+
232
+ def self.control?(packet)
233
+ packet.ip_src == packet.ip_dst && !loopback?(packet.ip_src)
234
+ end
235
+
236
+ def self.logging?(packet)
237
+ false
238
+ end
239
+
240
+ private
241
+
242
+ def self.loopback?(ip_addr)
243
+ ip_addr.to_num_s == LOCALHOST
244
+ end
245
+ end
246
+ end
247
+ end
248
+ ```
249
+
250
+ ```ruby
251
+ # lib/aoandon/dynamic_rule/syn_flood.rb
252
+ module Aoandon
253
+ module DynamicRule
254
+ module SynFlood
255
+ BUFFER = 20
256
+ MESSAGE = 'SYN flood attack'
257
+ PROTO_TCP = 6
258
+
259
+ def self.control?(packet)
260
+ tcp?(packet) && fifo!(packet.tcp_syn?) && packet.tcp_syn? && overflow?
261
+ end
262
+
263
+ def self.logging?(packet)
264
+ false
265
+ end
266
+
267
+ private
268
+
269
+ def self.fifo!(input)
270
+ stack << input
271
+ stack.shift
272
+ end
273
+
274
+ def self.overflow?
275
+ stack == [true] * BUFFER
276
+ end
277
+
278
+ def self.stack
279
+ @syn_flood_stack ||= [false] * BUFFER
280
+ end
281
+
282
+ def self.tcp?(packet)
283
+ packet.ip_proto == PROTO_TCP
284
+ end
285
+ end
286
+ end
287
+ end
288
+ ```
289
+
290
+ ## Contributing
291
+
292
+ 1. Fork it
293
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
294
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
295
+ 4. Push to the branch (`git push origin my-new-feature`)
296
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'aoandon/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'aoandon'
8
+ gem.version = Aoandon::VERSION
9
+ gem.authors = ['Cyril Wack']
10
+ gem.email = ['contact@cyril.io']
11
+ gem.description = %q{Aoandon (青行燈) is a minimalist network intrusion detection system (NIDS).}
12
+ gem.summary = %q{Minimalist network intrusion detection system (NIDS).}
13
+ gem.homepage = 'http://cyril.io'
14
+ gem.license = 'MIT'
15
+
16
+ gem.bindir = 'bin'
17
+
18
+ gem.files = `git ls-files`.split($/).reject {|f| f == 'blue-andon-creature.jpg' }
19
+ gem.executables = gem.files.grep(%r{^bin/}).map {|f| File.basename(f) }
20
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
+ gem.require_paths = ['lib', 'config']
22
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ipaddr'
4
+ require 'optparse'
5
+ require 'pcap'
6
+ require 'time'
7
+ require 'yaml'
8
+ require_relative '../lib/aoandon'
9
+
10
+ Aoandon::Nids.new.run
@@ -0,0 +1,54 @@
1
+ # Aoandon NIDS configuration file
2
+ ---
3
+ #macros:
4
+ # web_server: &web_server
5
+ # 114.21.70.71
6
+ # gateway: &gw
7
+ # 192.168.0.1
8
+
9
+ #tables:
10
+ # redzone: &redzone
11
+ # - "81.15.142.23"
12
+ # hacker: &id001
13
+ # - 81.15.142.23
14
+ # - 42.154.25.213
15
+ # blacklist: &blacklist
16
+ # - *id001
17
+ # - *gw
18
+ # - 81.15.142.23
19
+ # - "64.81.240.57"
20
+ # unknown:
21
+ # - any
22
+ # mz: &mz
23
+ # 192.168.0.201
24
+ # dmz: &dmz
25
+ # sql_server: &sql_server
26
+ # 10.0.0.2
27
+
28
+ #ports:
29
+ # web: &www
30
+ # - 80
31
+ # - 443
32
+ # p2p:
33
+ # - 63192
34
+
35
+ #messages:
36
+ # - &msg001 "ICMP packet from Google to MZ"
37
+ # - &msg002 "MZ intrusion detected!"
38
+
39
+ rules:
40
+ # # "default alert" approach
41
+ # - [ info, {}, {quick: true, log: true, msg: "Suspected packet!"} ]
42
+ #
43
+ # # then, selectively ignore certain traffic
44
+ # - [ pass, {af: inet, from: {addr: any}, to: {addr: any}} ]
45
+ # - [ warn, {proto: tcp, from: {addr: *blacklist}, to: {addr: any, port: *www}, flags: syn} ]
46
+ # - [ warn, {proto: tcp, from: {addr: any, port: 123}, to: {addr: *dmz}} ]
47
+ # - [ crit, {af: inet6, from: {addr: any}, to: {addr: any}}, {log: true} ]
48
+ # - [ pass, {af: inet, proto: tcp, from: {addr: *mz}, to: {addr: *web_server, port: *www}, {quick: true}} ]
49
+ # - [ warn, {proto: udp, from: {addr: *redzone}, to: {addr: 10.1.0.32, port: 21}} ]
50
+ # - [ info, {proto: tcp, from: {addr: 172.16.0.6}, to: {addr: 192.168.0.14, port: 22}} ]
51
+ # - [ crit, {proto: tcp, from: {addr: *blacklist}, to: {addr: *mz}}, {log: true, msg: *msg002} ]
52
+ # - [ info, {proto: tcp, to: {addr: 192.168.0.14, port: 22}} ]
53
+ # - [ pass, {proto: tcp, from: {addr: *id001}, to: {addr: *sql_server, port: 3306}} ]
54
+ # - [ info, {af: inet, proto: icmp, from: {addr: google.com}, to: {addr: *mz}}, {log: true, msg: *msg001} ]
@@ -0,0 +1,64 @@
1
+ require_relative 'aoandon/analysis'
2
+ require_relative 'aoandon/analysis/semantic'
3
+ require_relative 'aoandon/analysis/syntax'
4
+ require_relative 'aoandon/error/not_implemented_error'
5
+ require_relative 'aoandon/log'
6
+ require_relative 'aoandon/static_rule'
7
+ require_relative 'aoandon/version'
8
+
9
+ Dir['lib/aoandon/dynamic_rule/*.rb'].each do |src|
10
+ load src
11
+ end
12
+
13
+ module Aoandon
14
+ class Nids
15
+ CONF_PATH = 'config/rules.yml'
16
+
17
+ def initialize
18
+ options = Nids.parse
19
+ options[:file] = CONF_PATH unless options[:file]
20
+ options[:interface] = Pcap.lookupdev unless options[:interface]
21
+ puts "Starting Aoandon NIDS on interface #{options[:interface]}..."
22
+ log = Log.new(options[:verbose])
23
+ @syntax = Syntax.new(log, {file: options[:file]})
24
+ @semantic = Semantic.new(log)
25
+ @network_interface = Pcap::Capture.open_live(options[:interface])
26
+ end
27
+
28
+ def run
29
+ puts 'You can stop Aoandon NIDS by pressing Ctrl-C.'
30
+
31
+ @network_interface.each_packet do |packet|
32
+ if packet.ip?
33
+ @semantic.test(packet)
34
+ @syntax.test(packet)
35
+ end
36
+ end
37
+
38
+ @network_interface.close
39
+ end
40
+
41
+ def self.parse
42
+ options = {}
43
+
44
+ OptionParser.new do |opts|
45
+ opts.banner = "Usage: #$0 [options]"
46
+ opts.on('-f', '--file <path>', 'Load the rules contained in file <path>.') {|f| options[:file] = f }
47
+ opts.on('-h', '--help', 'Help.') { puts opts; exit }
48
+ opts.on('-i', '--interface <if>', 'Sniff on network interface <if>.') {|i| options[:interface] = i }
49
+ opts.on('-v', '--verbose', 'Produce more verbose output.') { options[:verbose] = true }
50
+ opts.on('-V', '--version', 'Show the version number and exit.') { version; exit }
51
+ end.parse!
52
+
53
+ options
54
+ end
55
+
56
+ def self.version
57
+ puts "Aoandon #{VERSION}"
58
+ end
59
+
60
+ trap('INT') { exit }
61
+ at_exit { print 'Stopping Aoandon NIDS... ' }
62
+ ObjectSpace.define_finalizer('string', proc { puts 'done.' })
63
+ end
64
+ end
@@ -0,0 +1,11 @@
1
+ module Aoandon
2
+ class Analysis
3
+ def initialize(logger, options = {})
4
+ @logger = logger
5
+ end
6
+
7
+ def update(packet = '')
8
+ raise NotImplementedError, 'Must subclass me'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ module Aoandon
2
+ class Semantic < Analysis
3
+ def initialize(logger, options = {})
4
+ super(logger, options)
5
+
6
+ puts "Modules: #{DynamicRule.constants.join(', ')}"
7
+ end
8
+
9
+ def test(packet)
10
+ if defined? DynamicRule
11
+ DynamicRule.constants.each do |rule|
12
+ if DynamicRule.const_get(rule).control?(packet)
13
+ dump = DynamicRule.const_get(rule).logging?(packet) ? packet : nil
14
+ message = if DynamicRule.const_get(rule).constants.include?(:MESSAGE)
15
+ DynamicRule.const_get(rule)::MESSAGE
16
+ else
17
+ nil
18
+ end
19
+
20
+ @logger.message(packet.time.iso8601, 'SEMANT', rule.downcase, message, dump)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,155 @@
1
+ module Aoandon
2
+ class Syntax < Analysis
3
+ def initialize(logger, options = {})
4
+ super(logger, options)
5
+
6
+ abort("Configuration file not found: #{options[:file]}") unless File.exist?(options[:file])
7
+ @rules = Array(YAML::load_file(options[:file])['rules']).map {|rule| StaticRule.new(*rule) }
8
+
9
+ puts "Ruleset: #{File.expand_path(options[:file])}"
10
+ end
11
+
12
+ def test(packet)
13
+ @rules.each do |rule|
14
+ if match?(packet, rule.context)
15
+ break if (@last_rule = rule).options['quick']
16
+ end
17
+ end
18
+
19
+ if @last_rule && @last_rule.action != 'pass'
20
+ message = @last_rule.options['msg'] || 'Bad packet detected!'
21
+ dump = @last_rule.options['log'] ? packet : nil
22
+ @logger.message(packet.time.iso8601, 'SYNTAX', @last_rule.action, message, dump)
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ def match?(packet, network_context)
29
+ network_context.update({'af' => af2id(packet.ip_ver)}) unless network_context.has_key?('af')
30
+ match_proto?(packet, network_context) if packet.ip_ver == af(network_context.fetch('af'))
31
+ end
32
+
33
+ def af2id(af)
34
+ if af == 4
35
+ 'inet'
36
+ elsif af == 6
37
+ 'inet6'
38
+ end
39
+ end
40
+
41
+ def af(name)
42
+ if name.to_sym == :inet
43
+ 4
44
+ elsif name.to_sym == :inet6
45
+ 6
46
+ end
47
+ end
48
+
49
+ def match_proto?(packet, network_context)
50
+ if network_context['proto']
51
+ if packet.ip_proto == proto(network_context['proto'])
52
+ if packet.ip_proto == 1
53
+ match_proto_icmp?(packet, network_context)
54
+ elsif packet.ip_proto == 6
55
+ match_proto_tcp?(packet, network_context)
56
+ elsif packet.ip_proto == 17
57
+ match_proto_udp?(packet, network_context)
58
+ elsif packet.ip_proto == 58
59
+ match_proto_icmp6?(packet, network_context)
60
+ else
61
+ match_addr?(packet, network_context)
62
+ end
63
+ end
64
+ else
65
+ match_addr?(packet, network_context)
66
+ end
67
+ end
68
+
69
+ def proto(name)
70
+ if name.to_sym == :icmp
71
+ 1
72
+ elsif name.to_sym == :icmp6
73
+ 58
74
+ elsif name.to_sym == :tcp
75
+ 6
76
+ elsif name.to_sym == :udp
77
+ 17
78
+ end
79
+ end
80
+
81
+ def match_proto_icmp?(packet, network_context)
82
+ match_addr?(packet, network_context)
83
+ end
84
+
85
+ def match_proto_icmp6?(packet, network_context)
86
+ match_proto_icmp?(packet, network_context)
87
+ end
88
+
89
+ def match_addr?(packet, network_context)
90
+ result = true
91
+
92
+ [['from', 'src'], ['to', 'dst']].each do |way, obj|
93
+ unless network_context[way].fetch('addr') == 'any'
94
+ result = result && refer2addr?((packet.send(obj)), network_context[way].fetch('addr'))
95
+ end
96
+ end
97
+
98
+ result
99
+ end
100
+
101
+ def match_port?(packet, network_context)
102
+ result = true
103
+
104
+ [['from', 'sport'], ['to', 'dport']].each do |way, obj|
105
+ if network_context[way].has_key?('port')
106
+ result = result && refer2port?((packet.send(obj)).to_i, network_context[way].fetch('port'))
107
+ end
108
+ end
109
+
110
+ result
111
+ end
112
+
113
+ def match_flag?(packet, network_context)
114
+ return true unless network_context['flags']
115
+
116
+ network_context['flags'].each do |flag|
117
+ return true if packet.send("tcp_#{flag}?")
118
+ end
119
+
120
+ false
121
+ end
122
+
123
+ def match_proto_tcp?(packet, network_context)
124
+ match_proto_udp?(packet, network_context) && match_flag?(packet, network_context)
125
+ end
126
+
127
+ def match_proto_udp?(packet, network_context)
128
+ match_addr?(packet, network_context) && match_port?(packet, network_context)
129
+ end
130
+
131
+ def refer2addr?(addr, pattern)
132
+ if pattern.is_a? Array
133
+ pattern.include?(addr.to_num_s) || pattern.include?(addr.hostname)
134
+ elsif pattern.is_a? Hash
135
+ pattern.has_key?(addr.to_num_s) || pattern.has_key?(addr.hostname)
136
+ elsif pattern.is_a? String
137
+ addr.to_num_s == pattern || addr.hostname == pattern
138
+ else
139
+ false
140
+ end
141
+ end
142
+
143
+ def refer2port?(number, pattern)
144
+ if pattern.is_a? Array
145
+ pattern.include?(number)
146
+ elsif pattern.is_a? Hash
147
+ pattern.has_key?(number)
148
+ elsif pattern.is_a? Fixnum
149
+ number == pattern
150
+ else
151
+ false
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,37 @@
1
+ module Aoandon
2
+ module DynamicRule
3
+ module Less1024
4
+ MESSAGE = 'Port numbers < 1024'
5
+ PROTO_TCP = 6
6
+ PROTO_UDP = 17
7
+ WELL_KNOWN_PORTS = (0..1023)
8
+
9
+ def self.control?(packet)
10
+ (tcp?(packet) || (udp?(packet) && different_ports?(packet.sport, packet.dport))) &&
11
+ less_1024?(packet.sport) && less_1024?(packet.dport)
12
+ end
13
+
14
+ def self.logging?(packet)
15
+ false
16
+ end
17
+
18
+ private
19
+
20
+ def self.different_ports?(src_port, dst_port)
21
+ src_port != dst_port
22
+ end
23
+
24
+ def self.less_1024?(port)
25
+ WELL_KNOWN_PORTS.include?(port)
26
+ end
27
+
28
+ def self.tcp?(packet)
29
+ packet.ip_proto == PROTO_TCP
30
+ end
31
+
32
+ def self.udp?(packet)
33
+ packet.ip_proto == PROTO_UDP
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,4 @@
1
+ module Aoandon
2
+ class NotImplementedError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,16 @@
1
+ module Aoandon
2
+ class Log
3
+ def initialize(verbose = false)
4
+ @file = File.open('log/aoandon.yml', 'a')
5
+ @verbose = verbose
6
+
7
+ puts "Log file: #{File.expand_path(@file.path)}"
8
+ end
9
+
10
+ def message(*args)
11
+ puts args.compact.map(&:to_s).join(' | ') if @verbose
12
+ @file.puts "- #{args.compact.map(&:to_s)}"
13
+ @file.flush
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Aoandon
2
+ class StaticRule < Struct.new(:action, :context, :options)
3
+ def initialize(*args)
4
+ super(*args)
5
+
6
+ self.context['from'] ||= {'addr' => 'any'}
7
+ self.context['to' ] ||= {'addr' => 'any'}
8
+
9
+ self.context['from'].update('addr' => 'any') unless self.context['from']['addr']
10
+ self.context['to' ].update('addr' => 'any') unless self.context['to' ]['addr']
11
+
12
+ self.options ||= {}
13
+ self.options.update('log' => false) unless self.options.has_key?('log')
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module Aoandon
2
+ VERSION = '0.0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aoandon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Cyril Wack
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-16 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Aoandon (青行燈) is a minimalist network intrusion detection system (NIDS).
15
+ email:
16
+ - contact@cyril.io
17
+ executables:
18
+ - aoandon
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitattributes
23
+ - .gitignore
24
+ - .rbenv-version
25
+ - Gemfile
26
+ - LICENSE
27
+ - README.md
28
+ - Rakefile
29
+ - aoandon.gemspec
30
+ - bin/aoandon
31
+ - config/rules.yml
32
+ - lib/aoandon.rb
33
+ - lib/aoandon/analysis.rb
34
+ - lib/aoandon/analysis/semantic.rb
35
+ - lib/aoandon/analysis/syntax.rb
36
+ - lib/aoandon/dynamic_rule/less1024.rb
37
+ - lib/aoandon/error/not_implemented_error.rb
38
+ - lib/aoandon/log.rb
39
+ - lib/aoandon/static_rule.rb
40
+ - lib/aoandon/version.rb
41
+ homepage: http://cyril.io
42
+ licenses:
43
+ - MIT
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ - config
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubyforge_project:
63
+ rubygems_version: 1.8.23
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Minimalist network intrusion detection system (NIDS).
67
+ test_files: []