ffi-ssdeep 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ == 0.1.1 / 2010-07-13
2
+ * Initial version
3
+
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Eric Monti
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ 'Software'), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,27 @@
1
+ = ffi-ssdeep
2
+
3
+ Ruby FFI bindings for the ssdeep 'libfuzzy' library api. This API lets you do
4
+ fuzzy hash comparisons between files and arbitrary string buffers. Fuzzy
5
+ hashes are also known as context triggered piecewise hashes (CTPH).
6
+
7
+ See the ssdeep homepage for more information:
8
+ http://ssdeep.sourceforge.net
9
+
10
+ == Requirements
11
+
12
+ * ffi >= 0.6.0
13
+ * ssdeep's libfuzzy - http://ssdeep.sourceforge.net
14
+ The version of ssdeep known to work with ffi-ssdeep is 2.5.
15
+
16
+ == Installation
17
+
18
+ First ensure you have installed the ssdeep package and that libfuzzy is in your libpath.
19
+ Then, just run
20
+
21
+ (sudo)? gem install ffi-ssdeep
22
+
23
+ == License
24
+
25
+ See LICENSE.txt
26
+
27
+
@@ -0,0 +1,31 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ load 'tasks/setup.rb'
6
+
7
+ ensure_in_path 'lib'
8
+
9
+ task :default => 'spec:run'
10
+
11
+ PROJ.name = 'ffi-ssdeep'
12
+ PROJ.authors = 'Eric Monti'
13
+ PROJ.email = 'emonti@trustwave.com'
14
+ PROJ.description = 'FFI bindings for the ssdeep library "libfuzzy" for fuzzy hash comparisons'
15
+ PROJ.url = nil
16
+ PROJ.version = File.open("version.txt","r"){|f| f.readline.chomp}
17
+ PROJ.readme_file = 'README.rdoc'
18
+
19
+ PROJ.spec.opts << '--color'
20
+ PROJ.rdoc.opts << '--line-numbers'
21
+ PROJ.notes.tags << "X"+"XX" # muhah! so we don't note our-self
22
+
23
+ # exclude rcov.rb and external libs from rcov report
24
+ PROJ.rcov.opts += [
25
+ "--exclude", "rcov",
26
+ "--exclude", "ffi",
27
+ ]
28
+
29
+ depend_on 'ffi', '>= 0.6.0'
30
+
31
+ # EOF
Binary file
@@ -0,0 +1,73 @@
1
+ require 'ffi'
2
+
3
+ require 'ssdeep/fuzzy_hash'
4
+
5
+ module Ssdeep
6
+ extend FFI::Library
7
+
8
+ ffi_lib 'fuzzy'
9
+
10
+ typedef :pointer, :fuzzy_hash
11
+
12
+ # Compute the fuzzy hash of a buffer.
13
+ #
14
+ # Computes the fuzzy hash of the first buf_len bytes of the buffer. It is the caller's
15
+ # responsibility to append the filename, if any, to result after computation.
16
+ #
17
+ # Parameters:
18
+ # buf The data to be fuzzy hashed
19
+ # buf_len The length of the data being hashed
20
+ # result Where the fuzzy hash of buf is stored. This variable
21
+ # must be allocated to hold at least FUZZY_MAX_RESULT bytes.
22
+ #
23
+ # Returns:
24
+ # Returns zero on success, non-zero on error.
25
+ attach_function :fuzzy_hash_buf, [:pointer, :uint32, :fuzzy_hash], :int
26
+
27
+ # Compute the fuzzy hash of a file.
28
+ #
29
+ # Opens, reads, and hashes the contents of the file 'filename' The
30
+ # result must be allocated to hold FUZZY_MAX_RESULT characters. It
31
+ # is the caller's responsibility to append the filename to the
32
+ # result after computation.
33
+ #
34
+ # Parameters:
35
+ # filename The file to be hashed
36
+ # result Where the fuzzy hash of the file is stored. This variable
37
+ # must be allocated to hold at least FUZZY_MAX_RESULT bytes.
38
+ #
39
+ # Returns:
40
+ # Returns zero on success, non-zero on error.
41
+ attach_function :fuzzy_hash_filename, [:string, :fuzzy_hash], :int
42
+
43
+ # Computes the match score between two fuzzy hash signatures.
44
+ #
45
+ # Returns:
46
+ # Returns a value from zero to 100 indicating the match score of the
47
+ # two signatures. A match score of zero indicates the sigantures did
48
+ # not match. When an error occurs, such as if one of the inputs is
49
+ # NULL, returns -1.
50
+ attach_function :fuzzy_compare, [:fuzzy_hash, :fuzzy_hash], :int
51
+
52
+
53
+ def self.hash_string(str)
54
+ FuzzyHash.from_string(str)
55
+ end
56
+
57
+ def self.hash_file(fname)
58
+ FuzzyHash.from_file(fname)
59
+ end
60
+
61
+ def self.compare_hashes(h1, h2)
62
+ h1.compare(h2)
63
+ end
64
+
65
+ def self.compare_strings(s1, s2)
66
+ compare_hashes(hash_string(s1), hash_string(s2))
67
+ end
68
+
69
+ def self.compare_files(f1, f2)
70
+ compare_hashes(hash_file(f1), hash_file(f2))
71
+ end
72
+
73
+ end
@@ -0,0 +1,88 @@
1
+
2
+ require 'ffi'
3
+
4
+ module Ssdeep
5
+ SPAMSUM_LENGTH = 64
6
+ FUZZY_MAX_RESULT = (SPAMSUM_LENGTH + (SPAMSUM_LENGTH/2 + 20))
7
+
8
+ class FuzzyHash < FFI::MemoryPointer
9
+ def initialize()
10
+ super(FUZZY_MAX_RESULT)
11
+ end
12
+
13
+ # returns the computed fuzzy hash as a string
14
+ def to_s
15
+ self.read_string()
16
+ end
17
+
18
+ # implements a _dump method for Marshal
19
+ def _dump(depth)
20
+ self.to_s
21
+ end
22
+
23
+ # @return [Integer]
24
+ # Returns a value from zero to 100 indicating the match score of the
25
+ # two signatures. A match score of zero indicates the sigantures did
26
+ # not match.
27
+ #
28
+ # @return [
29
+ # When an error occurs, such as if one of the inputs is NULL.
30
+ def compare(other)
31
+ unless other.is_a?(FuzzyHash)
32
+ raise(TypeError, "a FuzzyHash can only be compared to another FuzzyHash")
33
+ end
34
+ if self.to_s == other.to_s
35
+ return 100
36
+ elsif (ret=Ssdeep.fuzzy_compare(self, other)) > -1
37
+ return ret
38
+ else
39
+ raise(StandardError, "unknown fuzzy hash comparison error")
40
+ end
41
+ end
42
+
43
+ # implements a _load method for Marshal
44
+ def self._load(raw)
45
+ from_hash(raw)
46
+ end
47
+
48
+ # Restores a fuzzy hash that has already been generated and supplied
49
+ # as a string as a instance of a FuzzyHash object.
50
+ def self.from_hash(raw)
51
+ fh = new()
52
+ fh.write_string(
53
+ if raw.size < FUZZY_MAX_RESULT
54
+ raw + "\x00"
55
+ else
56
+ raw[0,FUZZY_MAX_RESULT]
57
+ end )
58
+ return fh
59
+ end
60
+
61
+ # Creates a new fuzzy hash from a string buffer.
62
+ def self.from_string(buf)
63
+ fh = new()
64
+ p = FFI::MemoryPointer.new(buf.size)
65
+ p.write_string(buf)
66
+ ret = Ssdeep.fuzzy_hash_buf(p, buf.size, fh)
67
+ if ret == 0
68
+ return fh
69
+ else
70
+ fh.free
71
+ raise(StandardError, "An error occurred hashing a string")
72
+ end
73
+ end
74
+
75
+ # Creates a new fuzzy hash from the contents of 'filename'
76
+ def self.from_file(filename)
77
+ fh = new()
78
+ ret = Ssdeep.fuzzy_hash_filename(filename, fh)
79
+ if ret == 0
80
+ return fh
81
+ else
82
+ fh.free
83
+ raise(StandardError, "An error occurred hashing file: #{filename.inspect}")
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ssdeep::FuzzyHash do
4
+ context "basic features" do
5
+ before(:all) do
6
+ @fh1 = Ssdeep::FuzzyHash.from_file(sample_file("ssdeep.gemspec"))
7
+ end
8
+
9
+ it "should support a from_string class method to hash from a string buffer" do
10
+ Ssdeep::FuzzyHash.from_file(sample_file("ssdeep.gemspec")).should be_kind_of(Ssdeep::FuzzyHash)
11
+ end
12
+
13
+ it "should support a from_string class method to hash from a string buffer" do
14
+ Ssdeep::FuzzyHash.from_string("helu").should be_kind_of(Ssdeep::FuzzyHash)
15
+ end
16
+
17
+ it "should support a compare method to compare one FuzzyHash to another" do
18
+ fh2 = Ssdeep.hash_file(sample_file("ffi-ssdeep.gemspec"))
19
+ sh = Ssdeep.hash_string("helu")
20
+ @fh1.compare(@fh1).should == 100
21
+ @fh1.compare(fh2).should > 90
22
+ @fh1.compare(sh).should == 0
23
+ end
24
+
25
+ it "should raise an exception when comparing null hash data" do
26
+ hh = Ssdeep::FuzzyHash.new()
27
+ lambda{ @fh1.compare(hh)}.should raise_error(StandardError)
28
+ lambda{ hh.compare(@fh1)}.should raise_error(StandardError)
29
+ end
30
+ end
31
+
32
+ context "marshal serialization" do
33
+ before(:all) do
34
+ @fh = Ssdeep::FuzzyHash.from_file(sample_file("ssdeep.gemspec"))
35
+ @ser = Marshal.dump(@fh)
36
+ end
37
+
38
+ it "should be serializable with Marshal.dump()" do
39
+ @ser.should be_kind_of(String)
40
+ @ser.should_not be_empty
41
+ @ser.index(@fh.to_s).should_not be_nil
42
+ end
43
+
44
+ it "should be unserializable with Marshal.load()" do
45
+ fh_restore = Marshal.load(@ser)
46
+ fh_restore.should be_kind_of(Ssdeep::FuzzyHash)
47
+ fh_restore.compare(@fh).should == 100
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,2 @@
1
+ --format specdoc
2
+ --color
@@ -0,0 +1,23 @@
1
+ SPEC_DIR = File.expand_path(File.dirname(__FILE__))
2
+
3
+ $LOAD_PATH.unshift(SPEC_DIR)
4
+ $LOAD_PATH.unshift(File.join(SPEC_DIR, '..', 'lib'))
5
+
6
+ require 'ssdeep'
7
+ require 'spec'
8
+ require 'spec/autorun'
9
+
10
+ SAMPLE_DIR = File.join(SPEC_DIR, 'samples')
11
+
12
+ def sample_message(filename)
13
+ dat = File.read(sample_file(filename))
14
+ dat.force_encoding('ASCII-8BIT') if RUBY_VERSION >= "1.9"
15
+ dat
16
+ end
17
+
18
+ def sample_file(filename)
19
+ File.join(SAMPLE_DIR, filename)
20
+ end
21
+
22
+ Spec::Runner.configure do |config|
23
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ssdeep do
4
+
5
+ it "should have a compare_strings method that compares two strings" do
6
+ str1 = File.read(__FILE__)
7
+ str2 = str1.dup; str2[10,0]="insert some other junk"
8
+ Ssdeep.compare_strings(str1, str1).should == 100
9
+ Ssdeep.compare_strings(str2, str1).should < 100
10
+ Ssdeep.compare_strings(str2, str1).should >= 80
11
+ Ssdeep.compare_strings(str1, "something else").should == 0
12
+ Ssdeep.compare_strings(str2, "something else").should == 0
13
+ end
14
+
15
+ it "should have a compare_files method that compares two files" do
16
+ f1 = __FILE__
17
+ f2 = File.join(File.dirname(__FILE__), "spec_helper.rb")
18
+ Ssdeep.compare_files(f1, f1).should == 100
19
+ Ssdeep.compare_files(f1, f2 ).should < 20
20
+ Ssdeep.compare_files(f1, f2 ).should >= 0
21
+ end
22
+
23
+ it "should raise an error when compare_files is given a bad filename" do
24
+ f1 = __FILE__
25
+ f2 = sample_file("bogus_file.dat")
26
+ lambda{ Ssdeep.compare_files(f1,f2) }.should raise_error(StandardError)
27
+ lambda{ Ssdeep.compare_files(f2,f1) }.should raise_error(StandardError)
28
+ end
29
+
30
+ it "should have a compare_hashes method that compares two hashes" do
31
+ str1 = File.read(__FILE__)
32
+ str2 = str1.dup; str2[10,0]="insert some other junk"
33
+ h1 = Ssdeep::FuzzyHash.from_string(str1)
34
+ h2 = Ssdeep::FuzzyHash.from_string(str2)
35
+ h3 = Ssdeep::FuzzyHash.from_string("something_else")
36
+ Ssdeep.compare_hashes(h1, h1).should == 100
37
+ Ssdeep.compare_hashes(h2, h2).should == 100
38
+ Ssdeep.compare_hashes(h1, h2).should < 100
39
+ Ssdeep.compare_hashes(h1, h2).should >= 80
40
+ Ssdeep.compare_hashes(h1, h3).should == 0
41
+ Ssdeep.compare_hashes(h2, h3).should == 0
42
+ end
43
+
44
+ it "should raise an exception when compare_hashes is given a bad argument" do
45
+ h1 = Ssdeep::FuzzyHash.from_string("some_data")
46
+ lambda{ Ssdeep.compare_hashes(h1, "not a hash") }.should raise_error(TypeError)
47
+ lambda{ Ssdeep.compare_hashes("not a hash", h1) }.should raise_error(StandardError)
48
+ end
49
+
50
+ it "should have a hash_string method that generates a fuzzy hash from a string" do
51
+ fh = Ssdeep.hash_string("this is a test string")
52
+ fh.should be_kind_of Ssdeep::FuzzyHash
53
+ fh.to_s.should == "3:YKEpFZ2:YfrI"
54
+ end
55
+
56
+ it "should have a hash_file method that generates a fuzzy hash from a filename" do
57
+ fh = Ssdeep.hash_file(sample_file("ssdeep.gemspec"))
58
+ fh.should be_kind_of Ssdeep::FuzzyHash
59
+ fh.to_s.should == "24:ZkR5abenafgr2ph4glPlXeh/Tzj1wfgrUERNvnNyx/7/i/vA:fbenDr2ph9S/TzjvrUQvnoh/i/I"
60
+ end
61
+
62
+ it "should raise an error when hash_file is given a bad filename" do
63
+ f1 = sample_file("bogus_file.dat")
64
+ lambda{ Ssdeep.hash_file(f1) }.should raise_error(StandardError)
65
+ end
66
+
67
+
68
+ end
69
+
@@ -0,0 +1,80 @@
1
+
2
+ begin
3
+ require 'bones/smtp_tls'
4
+ rescue LoadError
5
+ require 'net/smtp'
6
+ end
7
+ require 'time'
8
+
9
+ namespace :ann do
10
+
11
+ # A prerequisites task that all other tasks depend upon
12
+ task :prereqs
13
+
14
+ file PROJ.ann.file do
15
+ ann = PROJ.ann
16
+ puts "Generating #{ann.file}"
17
+ File.open(ann.file,'w') do |fd|
18
+ fd.puts("#{PROJ.name} version #{PROJ.version}")
19
+ fd.puts(" by #{Array(PROJ.authors).first}") if PROJ.authors
20
+ fd.puts(" #{PROJ.url}") if PROJ.url.valid?
21
+ fd.puts(" (the \"#{PROJ.release_name}\" release)") if PROJ.release_name
22
+ fd.puts
23
+ fd.puts("== DESCRIPTION")
24
+ fd.puts
25
+ fd.puts(PROJ.description)
26
+ fd.puts
27
+ fd.puts(PROJ.changes.sub(%r/^.*$/, '== CHANGES'))
28
+ fd.puts
29
+ ann.paragraphs.each do |p|
30
+ fd.puts "== #{p.upcase}"
31
+ fd.puts
32
+ fd.puts paragraphs_of(PROJ.readme_file, p).join("\n\n")
33
+ fd.puts
34
+ end
35
+ fd.puts ann.text if ann.text
36
+ end
37
+ end
38
+
39
+ desc "Create an announcement file"
40
+ task :announcement => ['ann:prereqs', PROJ.ann.file]
41
+
42
+ desc "Send an email announcement"
43
+ task :email => ['ann:prereqs', PROJ.ann.file] do
44
+ ann = PROJ.ann
45
+ from = ann.email[:from] || Array(PROJ.authors).first || PROJ.email
46
+ to = Array(ann.email[:to])
47
+
48
+ ### build a mail header for RFC 822
49
+ rfc822msg = "From: #{from}\n"
50
+ rfc822msg << "To: #{to.join(',')}\n"
51
+ rfc822msg << "Subject: [ANN] #{PROJ.name} #{PROJ.version}"
52
+ rfc822msg << " (#{PROJ.release_name})" if PROJ.release_name
53
+ rfc822msg << "\n"
54
+ rfc822msg << "Date: #{Time.new.rfc822}\n"
55
+ rfc822msg << "Message-Id: "
56
+ rfc822msg << "<#{"%.8f" % Time.now.to_f}@#{ann.email[:domain]}>\n\n"
57
+ rfc822msg << File.read(ann.file)
58
+
59
+ params = [:server, :port, :domain, :acct, :passwd, :authtype].map do |key|
60
+ ann.email[key]
61
+ end
62
+
63
+ params[3] = PROJ.email if params[3].nil?
64
+
65
+ if params[4].nil?
66
+ STDOUT.write "Please enter your e-mail password (#{params[3]}): "
67
+ params[4] = STDIN.gets.chomp
68
+ end
69
+
70
+ ### send email
71
+ Net::SMTP.start(*params) {|smtp| smtp.sendmail(rfc822msg, from, to)}
72
+ end
73
+ end # namespace :ann
74
+
75
+ desc 'Alias to ann:announcement'
76
+ task :ann => 'ann:announcement'
77
+
78
+ CLOBBER << PROJ.ann.file
79
+
80
+ # EOF