ffi-ssdeep 0.1.1

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,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