ndr_support 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +14 -0
  3. data/.rubocop.yml +27 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +22 -0
  6. data/CODE_OF_CONDUCT.md +13 -0
  7. data/Gemfile +4 -0
  8. data/Guardfile +16 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +91 -0
  11. data/Rakefile +12 -0
  12. data/code_safety.yml +258 -0
  13. data/gemfiles/Gemfile.rails32 +6 -0
  14. data/gemfiles/Gemfile.rails32.lock +108 -0
  15. data/gemfiles/Gemfile.rails41 +6 -0
  16. data/gemfiles/Gemfile.rails41.lock +111 -0
  17. data/gemfiles/Gemfile.rails42 +6 -0
  18. data/gemfiles/Gemfile.rails42.lock +111 -0
  19. data/lib/ndr_support.rb +21 -0
  20. data/lib/ndr_support/array.rb +52 -0
  21. data/lib/ndr_support/concerns/working_days.rb +94 -0
  22. data/lib/ndr_support/date_and_time_extensions.rb +103 -0
  23. data/lib/ndr_support/daterange.rb +196 -0
  24. data/lib/ndr_support/fixnum/calculations.rb +15 -0
  25. data/lib/ndr_support/fixnum/julian_date_conversions.rb +14 -0
  26. data/lib/ndr_support/hash.rb +52 -0
  27. data/lib/ndr_support/integer.rb +12 -0
  28. data/lib/ndr_support/nil.rb +38 -0
  29. data/lib/ndr_support/ourdate.rb +97 -0
  30. data/lib/ndr_support/ourtime.rb +51 -0
  31. data/lib/ndr_support/regexp_range.rb +65 -0
  32. data/lib/ndr_support/safe_file.rb +185 -0
  33. data/lib/ndr_support/safe_path.rb +268 -0
  34. data/lib/ndr_support/string/cleaning.rb +136 -0
  35. data/lib/ndr_support/string/conversions.rb +137 -0
  36. data/lib/ndr_support/tasks.rb +1 -0
  37. data/lib/ndr_support/time/conversions.rb +13 -0
  38. data/lib/ndr_support/utf8_encoding.rb +72 -0
  39. data/lib/ndr_support/utf8_encoding/control_characters.rb +53 -0
  40. data/lib/ndr_support/utf8_encoding/force_binary.rb +44 -0
  41. data/lib/ndr_support/utf8_encoding/object_support.rb +31 -0
  42. data/lib/ndr_support/version.rb +5 -0
  43. data/lib/ndr_support/yaml/serialization_migration.rb +65 -0
  44. data/lib/tasks/audit_code.rake +423 -0
  45. data/ndr_support.gemspec +39 -0
  46. data/test/array_test.rb +20 -0
  47. data/test/concerns/working_days_test.rb +122 -0
  48. data/test/daterange_test.rb +194 -0
  49. data/test/fixnum/calculations_test.rb +28 -0
  50. data/test/hash_test.rb +84 -0
  51. data/test/integer_test.rb +14 -0
  52. data/test/nil_test.rb +40 -0
  53. data/test/ourdate_test.rb +27 -0
  54. data/test/ourtime_test.rb +27 -0
  55. data/test/regexp_range_test.rb +135 -0
  56. data/test/resources/filesystem_paths.yml +37 -0
  57. data/test/safe_file_test.rb +597 -0
  58. data/test/safe_path_test.rb +168 -0
  59. data/test/string/cleaning_test.rb +176 -0
  60. data/test/string/conversions_test.rb +353 -0
  61. data/test/test_helper.rb +41 -0
  62. data/test/time/conversions_test.rb +15 -0
  63. data/test/utf8_encoding/control_characters_test.rb +84 -0
  64. data/test/utf8_encoding/force_binary_test.rb +64 -0
  65. data/test/utf8_encoding_test.rb +170 -0
  66. data/test/yaml/serialization_test.rb +145 -0
  67. metadata +295 -0
@@ -0,0 +1,51 @@
1
+ require 'active_support/time'
2
+ require 'ndr_support/ourdate'
3
+
4
+ # Convert a string into a time value (timestamp)
5
+ # (helped by String.thetime)
6
+ class Ourtime
7
+ attr_reader :thetime
8
+
9
+ def initialize(x = nil)
10
+ if x.is_a?(Time)
11
+ @thetime = x
12
+ elsif x.is_a?(Date)
13
+ @thetime = x.to_time
14
+ elsif x.is_a?(String)
15
+ self.source = x
16
+ else
17
+ @thetime = nil
18
+ end
19
+ end
20
+
21
+ def to_s
22
+ @thetime ? @thetime.to_time.to_s(:ui) : ''
23
+ end
24
+
25
+ def empty?
26
+ # An unspecified time will be empty. A valid or invalid time will not.
27
+ @thetime.nil? && @source.blank?
28
+ end
29
+
30
+ def source=(s)
31
+ begin
32
+ # Re-parse our own timestamps [+- seconds] without swapping month / day
33
+ @thetime = DateTime.strptime(s, '%d.%m.%Y %H:%M:%S').to_time
34
+ rescue ArgumentError
35
+ begin
36
+ @thetime = DateTime.strptime(s, '%d.%m.%Y %H:%M').to_time
37
+ rescue ArgumentError
38
+ @thetime = Time.parse(s)
39
+ end
40
+ end
41
+ # Apply timezone correction for daylight saving
42
+ if @thetime
43
+ @thetime = Ourdate.build_datetime(@thetime.year, @thetime.month,
44
+ @thetime.day, @thetime.hour,
45
+ @thetime.min, @thetime.sec,
46
+ @thetime.instance_of?(Time) ? @thetime.usec : 0).to_time
47
+ end
48
+ end
49
+
50
+ private :source=
51
+ end
@@ -0,0 +1,65 @@
1
+ # This class provides the ability to define a range using numbers or regular expressions
2
+ # and when provided with an array, will return a normal ruby Range object based on the matching
3
+ # elements in the array. NOTE this class is has the same attributes as Range and is identical
4
+ # when serialized (except for the class declaration obviously), but it is NOT a substitute for
5
+ # Range (only a facade).
6
+ class RegexpRange
7
+ class PatternMatchError < StandardError
8
+ end
9
+
10
+ attr_reader :begin, :end, :excl
11
+
12
+ def initialize(range_start, range_end, exclusive = false)
13
+ @begin = range_start
14
+ @end = range_end
15
+ @excl = exclusive
16
+ end
17
+
18
+ def to_range(lines)
19
+ start_line_number = @begin
20
+ if start_line_number.is_a?(Regexp)
21
+ lines.each_with_index do |line, i|
22
+ if line.match(start_line_number)
23
+ start_line_number = i
24
+ break
25
+ end
26
+ end
27
+
28
+ if start_line_number.is_a?(Regexp)
29
+ fail PatternMatchError, "begin pattern #{start_line_number.inspect} not found"
30
+ end
31
+ end
32
+
33
+ end_line_number = @end
34
+ if end_line_number.is_a?(Regexp)
35
+ start_scan_line = start_line_number + 1
36
+ lines[start_scan_line..-1].each_with_index do |line, i|
37
+ # puts "##{start_scan_line + i}: #{line}"
38
+ if line.match(end_line_number)
39
+ end_line_number = start_scan_line + i
40
+ break
41
+ end
42
+ end
43
+ if end_line_number.is_a?(Regexp)
44
+ fail PatternMatchError,
45
+ "end pattern #{end_line_number.inspect} not found on or after line #{start_scan_line}"
46
+ end
47
+ end
48
+
49
+ Range.new(start_line_number, end_line_number, @excl)
50
+ end
51
+
52
+ # `other` is equal to self if it is a RegexpRange with the same state.
53
+ def ==(other)
54
+ other.is_a?(RegexpRange) && other.state == state
55
+ end
56
+ alias_method :eql?, :==
57
+
58
+ protected
59
+
60
+ # Used by other RegexpRange objects, as well as Hashes, during equality checks:
61
+ def state
62
+ [@begin, @end, @excl]
63
+ end
64
+ delegate :hash, :to => :state # Used for Hash key lookup
65
+ end
@@ -0,0 +1,185 @@
1
+ class SafeFile
2
+ def initialize(*args)
3
+ a = self.class.get_fname_mode_prms(*args)
4
+ fname = a[0]
5
+ mode = a[1]
6
+ prms = a[2]
7
+
8
+ if prms
9
+ @file = File.new(fname, mode, prms)
10
+ else
11
+ @file = File.new(fname, mode)
12
+ end
13
+
14
+ # Just in case better clone the object
15
+ # Ruby object are passed by reference
16
+ @file_name = fname.clone
17
+ end
18
+
19
+ def self.open(*args)
20
+ return SafeFile.new(*args) unless block_given?
21
+
22
+ f = SafeFile.new(*args)
23
+ yield f
24
+ f.close
25
+ end
26
+
27
+ def close
28
+ @file.close
29
+ end
30
+
31
+ def read
32
+ verify @file_name, 'r'
33
+ @file.read
34
+ end
35
+
36
+ def write(data)
37
+ verify @file_name, 'w'
38
+ @file.write(data)
39
+ end
40
+
41
+ def path
42
+ @file_name.clone
43
+ end
44
+
45
+ def self.extname(file_name)
46
+ verify file_name
47
+ File.extname(file_name)
48
+ end
49
+
50
+ def self.read(file_name)
51
+ verify file_name, 'r'
52
+ File.read(file_name)
53
+ end
54
+
55
+ def self.readlines(*args)
56
+ fail ArgumentError, "Incorrect number of arguments - #{args.length}" if args.length > 2 or args.length == 0
57
+ verify args[0], 'r'
58
+ File.readlines(*args)
59
+ end
60
+
61
+ def self.directory?(file_name)
62
+ verify file_name
63
+ File.directory?(file_name)
64
+ end
65
+
66
+ def self.exist?(file_name)
67
+ self.exists?(file_name)
68
+ end
69
+
70
+ def self.exists?(file_name)
71
+ verify file_name
72
+ File.exist?(file_name)
73
+ end
74
+
75
+ def self.file?(file_name)
76
+ verify file_name
77
+ File.file?(file_name)
78
+ end
79
+
80
+ def self.zero?(file_name)
81
+ verify file_name
82
+ File.zero?(file_name)
83
+ end
84
+
85
+ def self.basename(file_name, suffix = :none)
86
+ verify file_name
87
+ if suffix == :none
88
+ File.basename(file_name)
89
+ else
90
+ File.basename(file_name, suffix)
91
+ end
92
+ end
93
+
94
+ def self.safepath_to_string(fname)
95
+ verify fname
96
+ fname.to_s
97
+ end
98
+
99
+ def self.basename_file
100
+ # SECURE: 02-08-2012 TPG Can't assign to __FILE__
101
+ File.basename(__FILE__)
102
+ end
103
+
104
+ def self.dirname(path)
105
+ verify path
106
+ res = path.clone
107
+ res.path = File.dirname(path)
108
+ res
109
+ end
110
+
111
+ def self.delete(*list)
112
+ verify list, 'w'
113
+
114
+ list.each do |file|
115
+ File.delete(file) if File.exist?(file)
116
+ end.length
117
+ end
118
+
119
+ private
120
+
121
+ def verify(file_names, prm = nil)
122
+ self.class.verify(file_names, prm)
123
+ end
124
+
125
+ def self.verify(file_names, prm = nil)
126
+ [file_names].flatten.each do |file_name|
127
+ fail ArgumentError, "file_name should be of type SafePath, but it is #{file_name.class}" unless file_name.class == SafePath
128
+
129
+ if prm
130
+ [prm].flatten.each do |p|
131
+ fail SecurityError, "Permissions denied. Cannot access the file #{file_name} with permissions #{prm}. The permissions are #{file_name.permissions}" unless file_name.permissions.include?(p)
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def self.verify_mode(file_name, mode)
138
+ if mode.match(/\A(r\+)|(w\+)|(a\+)\Z/)
139
+ verify file_name, ['w', 'r']
140
+ elsif mode.match(/\Aw|a\Z/)
141
+ verify file_name, ['w']
142
+ elsif mode.match(/\Ar\Z/)
143
+ verify file_name, ['r']
144
+ else
145
+ fail ArgumentError, "Incorrect mode. It should be one of: 'r', 'w', 'r+', 'w+', 'a', 'a+'"
146
+ end
147
+ end
148
+
149
+ def self.get_fname_mode_prms(*args)
150
+ case args.length
151
+ when 1
152
+ verify_mode(args[0], 'r')
153
+ fname = args[0]
154
+ mode = 'r'
155
+ prms = nil
156
+
157
+ when 2
158
+ fail ArgimentError if args[1].class != Fixnum and args[1].class != String
159
+
160
+ if args[1].class == Fixnum
161
+ verify_mode(args[0], 'r')
162
+ mode = 'r'
163
+ prms = args[1]
164
+ else
165
+ verify_mode(args[0], args[1])
166
+ mode = args[1]
167
+ prms = nil
168
+ end
169
+
170
+ fname = args[0]
171
+
172
+ when 3
173
+ fail ArgimentError if args[1].class != String or args[2].class != Fixnum
174
+ verify_mode(args[0], args[1])
175
+
176
+ fname = args[0]
177
+ mode = args[1]
178
+ prms = args[2]
179
+ else
180
+ fail ArgumentError, "Incorrect number of arguments #{args.length}"
181
+ end
182
+
183
+ [fname, mode, prms]
184
+ end
185
+ end
@@ -0,0 +1,268 @@
1
+ require 'erb'
2
+
3
+ # Ruby has a built-in SecurityError class
4
+ #class SecurityError < StandardError
5
+ #end
6
+
7
+ # = SafePath
8
+ #
9
+ # SafePath is a class which contains path to a file or directory. It also holds "path space" and
10
+ # permissions. The path space is a directory. Everything in this directory and all the
11
+ # subdirectories can be accessed with the permissions given to the constructor. The instance of
12
+ # the class checks whether the path constructed points to a directory, whcih is in the
13
+ # "path space". The idea is to limit the access of the program to given directory.
14
+ #
15
+ # Example of usage is :
16
+ # sp = SafePath("dbs_inbox")
17
+ #
18
+ # The root directory of the pathspace is in the file config/filesystem_paths.yml . In this case dbs_inbox has root
19
+ # /mounts/ron/dbs_inbox . Every path which starts with /mount/ron/dbs_inbox is considered as safe. If a path is constructed
20
+ # which is /mounts/ron/dbs_inbox/../../../etc/passwd for example then the class will evaluate the path and it will
21
+ # raise exception SecurityError.
22
+ #
23
+ # The paths can be constructed by using +, join or join!.
24
+ # Example:
25
+ # This:
26
+ # sp = SafePath("dbs_inbox")
27
+ # sp + "/my_dir"
28
+ # Points to:
29
+ # /mounts/ron/dbs_inbox/my_dir
30
+ #
31
+ #
32
+ # The functions join and join! work in similar way. The difference between
33
+ # join and join! is that join creates new instance of the class SafePath and
34
+ # return it and join! doesn't create new instance, but works in-place and after
35
+ # that it returns reference to the current instance.
36
+ # The both operators can be used like that:
37
+ # sp.join("/my_dir") #this is the same as sp + "my_dir"
38
+ # sp.join!("/my_dir") #this is NOT the same as sp "my_dir"
39
+ #
40
+ # Warning the function sp.path = "some_path" will treat some_path as absolute path
41
+ # and if it doesn't point to the root it will raise exception. The danger is that
42
+ # it returns the argument on the right hand side. So if it is a string the operator
43
+ # will return a string. This is the way ruby works. If it is used properly it shouldn't
44
+ # be a problem. The best way to use it is:
45
+ # sp.path = sp.root + "my_dir"
46
+ # sp.root returns SafePath and after that + is called which also returns SafePath. So the
47
+ # right hand side of the expression is SafePath and the = will return SafePath.
48
+ #
49
+ class SafePath
50
+
51
+ # Returns the list of safe 'root' filesystem locations, or raises
52
+ # a SecurityError if no configuration has been provided.
53
+ def self.fs_paths
54
+ if defined?(@@fs_paths)
55
+ @@fs_paths
56
+ else
57
+ fail SecurityError, 'SafePath not configured!'
58
+ end
59
+ end
60
+
61
+ # Takes the path the filesystem_paths.yml file that
62
+ # should be used. Attempting to reconfigure with
63
+ # new settings will raise a security error.
64
+ def self.configure!(filepath)
65
+ if defined?(@@fs_paths)
66
+ fail SecurityError, 'Attempt to re-assign SafePath config!'
67
+ else
68
+ File.open(filepath, 'r') do |file|
69
+ @@fs_paths = YAML.load(ERB.new(file.read).result)
70
+ @@fs_paths.freeze
71
+ end
72
+ end
73
+ end
74
+
75
+ # Takes:
76
+ # * path - This is a path to a directory. Usually a string.
77
+ # * path_space - This is identifier of the path space in whichi the system should work.
78
+ # it is a string. To find list of path spaces with their roots, please
79
+ # see config/filesystem_paths.yml
80
+ #
81
+ # Raises:
82
+ # * ArgumentError
83
+ # * SecurityError
84
+ #
85
+ def initialize(path_space, root_suffix = '')
86
+ # The class has to use different path definitions during test
87
+
88
+ fs_paths = self.class.fs_paths
89
+
90
+ platform = fs_paths.keys.select do |key|
91
+ RUBY_PLATFORM.match key
92
+ end[0]
93
+
94
+ root_path_regexp = if platform == /mswin32|mingw32/
95
+ /\A([A-Z]:[\\\/]|\/\/)/
96
+ else
97
+ /\A\//
98
+ end
99
+
100
+ fail ArgumentError, "The space #{path_space} doesn't exist. Please choose one of: #{fs_paths[platform].keys.inspect}" unless fs_paths[platform][path_space]
101
+ fail ArgumentError, "The space #{path_space} is broken. The root path should be absolute path but it was #{fs_paths[platform][path_space]['root']}" unless fs_paths[platform][path_space]['root'].match(root_path_regexp)
102
+ fail ArgumentError, "The space #{path_space} is broken. No permissions specified}" unless fs_paths[platform][path_space]['prms']
103
+
104
+ # The function verify uses @root. Therefore first assign the root path
105
+ # specified in the yaml file. After that verify that the root path
106
+ # specified from the contructor is subpath of the path specified in the
107
+ # yaml file. If it is not raise exception before anything else is done.
108
+ #
109
+ # The reason to assign @root 2 times is that it is better to have the
110
+ # logic verifing the path in only one function. This way it is going
111
+ # to be easier to maintain it and keep it secure.
112
+ #
113
+
114
+ @root = File.expand_path fs_paths[platform][path_space]['root']
115
+ @root = verify(File.join(fs_paths[platform][path_space]['root'], root_suffix)) unless root_suffix.blank?
116
+
117
+ @path_space = path_space
118
+ @maximum_prms = fs_paths[platform][path_space]['prms']
119
+
120
+ self.path = @root
121
+ end
122
+
123
+ def ==(other)
124
+ other.class == SafePath and other.root.to_s == @root and other.permissions == self.permissions and other.to_s == self.to_s
125
+ end
126
+
127
+ # WARNING: do not use sp.to_s + from_attacker . This is unsafe!
128
+ #
129
+ # Returns:
130
+ # String
131
+ #
132
+ def to_s
133
+ @path
134
+ end
135
+
136
+ # WARNING: do not use s.to_s + "my_path" . This is unsafe!
137
+ #
138
+ # Returns:
139
+ # String
140
+ #
141
+ def to_str
142
+ self.to_s
143
+ end
144
+
145
+ # Getter for permissions.
146
+ #
147
+ # Returns:
148
+ # Array
149
+ def permissions
150
+ if @prm
151
+ @prm
152
+ else
153
+ @maximum_prms
154
+ end
155
+ end
156
+
157
+ # Getter for path space identifier.
158
+ #
159
+ # Returns:
160
+ # Array
161
+ #
162
+ def path_space
163
+ @path_space
164
+ end
165
+
166
+ # Getter for the root of the path space.
167
+ #
168
+ # Returns:
169
+ # SafePath
170
+ #
171
+ def root
172
+ r = self.clone
173
+ r.path = @root
174
+ r.permissions = self.permissions
175
+ r
176
+ end
177
+
178
+ # The permissions are specified in the yml file, but this function
179
+ # can select subset of these permissions. It cannot select permission,
180
+ # which is not specified in the yml file.
181
+ #
182
+ # Takes:
183
+ # * Array of permission or single permission - If it is array then
184
+ # tha array could contain duplicates and it also can be nested.
185
+ # All the duplicates will be removed and the array will be flattened
186
+ #
187
+ # Returns:
188
+ # Array - this is the reuslt of the assignment. Note it is the right hand side of the expression
189
+ #
190
+ # Raises:
191
+ # * ArgumentError
192
+ #
193
+ def permissions=(permissions)
194
+ err_mess = "permissions has to be one or more of the values: #{@maximum_prms.inspect}\n but it was #{permissions.inspect}"
195
+
196
+ @prm = [permissions].flatten.each do |prm|
197
+ fail ArgumentError, err_mess unless @maximum_prms.include?(prm)
198
+ end.uniq
199
+ end
200
+
201
+ # Setter for path.
202
+ #
203
+ # Takes:
204
+ # * path - The path.
205
+ #
206
+ # Returns:
207
+ # Array - this is the result of the assignment. Note it is the right hand side of the expression
208
+ #
209
+ # Raises:
210
+ # * SecurityError
211
+ #
212
+ # Warning avoid using this in expressions like this (safe_path_new = (safe_path.path = safe_path.root.to_s + "/test")) + path_from_attacker
213
+ # This is unsafe!
214
+ #
215
+ def path=(path)
216
+ @path = verify(path)
217
+ self
218
+ end
219
+
220
+ # Another name for join
221
+ def +(path)
222
+ self.join(path)
223
+ end
224
+
225
+ # Used to construct path. It joins the current path with the given one.
226
+ # Takes:
227
+ # * path - path to be concatenated
228
+ #
229
+ # Returns:
230
+ # New instance of SafePath which contains the new path.
231
+ #
232
+ # Raises:
233
+ # * SecurityError
234
+ #
235
+ def join(path)
236
+ r = self.clone
237
+ r.path = File.join(@path, path)
238
+ r
239
+ end
240
+
241
+ # Used to construct path. It joins the current path with the given one.
242
+ # Takes:
243
+ # * path - path to be concatenated
244
+ #
245
+ # Returns:
246
+ # Reference to the current instance. It works in-place
247
+ #
248
+ # Raises:
249
+ # * SecurityError
250
+ #
251
+ def join!(path)
252
+ self.path = File.join(@path, path)
253
+ self
254
+ end
255
+
256
+ def length()
257
+ @path.length
258
+ end
259
+
260
+ private
261
+
262
+ # Verifies whether the path is safe.
263
+ def verify(path)
264
+ epath = File.expand_path(path)
265
+ fail SecurityError, "The given path is insecure. The path should point at #{@root}, but it points at #{epath}" unless epath.match(/\A#{Regexp.quote(@root)}/)
266
+ epath
267
+ end
268
+ end