wpscan 3.0.5 → 3.0.6
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.
- checksums.yaml +4 -4
- data/app/controllers/brute_force.rb +5 -13
- data/app/controllers/core.rb +0 -2
- data/app/controllers/enumeration.rb +1 -1
- data/app/controllers/enumeration/cli_options.rb +5 -6
- data/app/controllers/enumeration/enum_methods.rb +2 -2
- data/app/finders/users/login_error_messages.rb +1 -3
- data/app/finders/wp_version/unique_fingerprinting.rb +1 -35
- data/app/models/wp_item.rb +2 -2
- data/app/models/wp_version.rb +8 -2
- data/lib/wpscan.rb +1 -0
- data/lib/wpscan/db.rb +1 -19
- data/lib/wpscan/db/dynamic_finders.rb +2 -10
- data/lib/wpscan/db/fingerprints.rb +50 -0
- data/lib/wpscan/db/updater.rb +13 -1
- data/lib/wpscan/version.rb +1 -1
- metadata +7 -63
- data/lib/wpscan/db/schema.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 798d7f0db156c00bfee19eb7f540ffd6a1884ef1
|
4
|
+
data.tar.gz: 89eba229e55e42aa5629e76a8f6ca38e6e1a36a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8af7943d9c8ed54283853d0c9a89d397847d66262f53888c820f177cbc3d86642b6eba9131955cc3fe4406632a38c4fd61b765ac25cee578ae239a637f70a4ea
|
7
|
+
data.tar.gz: 2d356dcfe246a074bc47fd24a4e8bb7b52e253d47a6a0c491b6ff7a6912d283fabb0db7a495c0a1ab03ca7011699c09031e4664b02e6a0811a50732f63b4702e
|
@@ -10,11 +10,7 @@ module WPScan
|
|
10
10
|
'If no --username/s option supplied, user enumeration will be run'],
|
11
11
|
exists: true
|
12
12
|
),
|
13
|
-
|
14
|
-
OptFilePath.new(
|
15
|
-
['--usernames FILE-PATH', '-U', 'List of usernames to use during the brute forcing'],
|
16
|
-
exists: true
|
17
|
-
)
|
13
|
+
OptSmartList.new(['--usernames LIST', '-U', 'List of usernames to use during the brute forcing'])
|
18
14
|
]
|
19
15
|
end
|
20
16
|
|
@@ -36,14 +32,10 @@ module WPScan
|
|
36
32
|
|
37
33
|
# @return [ Array<Users> ] The users to brute force
|
38
34
|
def users
|
39
|
-
return target.users unless parsed_options[:usernames]
|
35
|
+
return target.users unless parsed_options[:usernames]
|
40
36
|
|
41
|
-
|
42
|
-
|
43
|
-
else
|
44
|
-
File.open(parsed_options[:usernames]).reduce([]) do |acc, elem|
|
45
|
-
acc << User.new(elem.chomp)
|
46
|
-
end
|
37
|
+
parsed_options[:usernames].reduce([]) do |acc, elem|
|
38
|
+
acc << User.new(elem.chomp)
|
47
39
|
end
|
48
40
|
end
|
49
41
|
|
@@ -89,7 +81,7 @@ module WPScan
|
|
89
81
|
def progress_bar(size, username)
|
90
82
|
ProgressBar.create(
|
91
83
|
format: '%t %a <%B> (%c / %C) %P%% %e',
|
92
|
-
title: "Brute Forcing #{username}",
|
84
|
+
title: "Brute Forcing #{username} -",
|
93
85
|
total: size
|
94
86
|
)
|
95
87
|
end
|
data/app/controllers/core.rb
CHANGED
@@ -31,7 +31,7 @@ module WPScan
|
|
31
31
|
def create_plugins_comments_finders(mod, config)
|
32
32
|
mod.const_set(
|
33
33
|
:Comments, Class.new(Finders::Finder::PluginVersion::Comments) do
|
34
|
-
const_set(:PATTERN,
|
34
|
+
const_set(:PATTERN, config['pattern'])
|
35
35
|
end
|
36
36
|
)
|
37
37
|
end
|
@@ -42,7 +42,7 @@ module WPScan
|
|
42
42
|
# @return [ Array<OptParseValidator::OptBase> ]
|
43
43
|
def cli_plugins_opts
|
44
44
|
[
|
45
|
-
|
45
|
+
OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate']),
|
46
46
|
OptChoice.new(
|
47
47
|
['--plugins-detection MODE',
|
48
48
|
'Use the supplied mode to enumerate Plugins, instead of the global (--detection-mode) mode.'],
|
@@ -65,7 +65,7 @@ module WPScan
|
|
65
65
|
# @return [ Array<OptParseValidator::OptBase> ]
|
66
66
|
def cli_themes_opts
|
67
67
|
[
|
68
|
-
|
68
|
+
OptSmartList.new(['--themes-list LIST', 'List of themes to enumerate']),
|
69
69
|
OptChoice.new(
|
70
70
|
['--themes-detection MODE',
|
71
71
|
'Use the supplied mode to enumerate Themes, instead of the global (--detection-mode) mode.'],
|
@@ -129,10 +129,9 @@ module WPScan
|
|
129
129
|
# @return [ Array<OptParseValidator::OptBase> ]
|
130
130
|
def cli_users_opts
|
131
131
|
[
|
132
|
-
|
133
|
-
['--users-list
|
134
|
-
'List of users to check during the users enumeration from the Login Error Messages']
|
135
|
-
exists: true
|
132
|
+
OptSmartList.new(
|
133
|
+
['--users-list LIST',
|
134
|
+
'List of users to check during the users enumeration from the Login Error Messages']
|
136
135
|
),
|
137
136
|
OptChoice.new(
|
138
137
|
['--users-detection MODE',
|
@@ -63,7 +63,7 @@ module WPScan
|
|
63
63
|
# @return [ Array<String> ] The plugins list associated to the cli options
|
64
64
|
def plugins_list_from_opts(opts)
|
65
65
|
# List file provided by the user via the cli
|
66
|
-
return
|
66
|
+
return opts[:plugins_list] if opts[:plugins_list]
|
67
67
|
|
68
68
|
if opts[:enumerate][:all_plugins]
|
69
69
|
DB::Plugins.all_slugs
|
@@ -101,7 +101,7 @@ module WPScan
|
|
101
101
|
# @return [ Array<String> ] The themes list associated to the cli options
|
102
102
|
def themes_list_from_opts(opts)
|
103
103
|
# List file provided by the user via the cli
|
104
|
-
return
|
104
|
+
return opts[:themes_list] if opts[:themes_list]
|
105
105
|
|
106
106
|
if opts[:enumerate][:all_themes]
|
107
107
|
DB::Themes.all_slugs
|
@@ -38,9 +38,7 @@ module WPScan
|
|
38
38
|
# usernames from the potential Users found
|
39
39
|
unames = opts[:found].map(&:username)
|
40
40
|
|
41
|
-
|
42
|
-
File.open(opts[:list]).each { |uname| unames << uname.chomp }
|
43
|
-
end
|
41
|
+
[*opts[:list]].each { |uname| unames << uname.chomp }
|
44
42
|
|
45
43
|
unames.uniq
|
46
44
|
end
|
@@ -5,19 +5,9 @@ module WPScan
|
|
5
5
|
class UniqueFingerprinting < CMSScanner::Finders::Finder
|
6
6
|
include CMSScanner::Finders::Finder::Fingerprinter
|
7
7
|
|
8
|
-
QUERY = 'SELECT md5_hash, path_id, version_id, ' \
|
9
|
-
'versions.number AS version,' \
|
10
|
-
'paths.value AS path ' \
|
11
|
-
'FROM fingerprints ' \
|
12
|
-
'LEFT JOIN versions ON version_id = versions.id ' \
|
13
|
-
'LEFT JOIN paths on path_id = paths.id ' \
|
14
|
-
'WHERE md5_hash IN ' \
|
15
|
-
'(SELECT md5_hash FROM fingerprints GROUP BY md5_hash HAVING COUNT(*) = 1) ' \
|
16
|
-
'ORDER BY version DESC'.freeze
|
17
|
-
|
18
8
|
# @return [ WpVersion ]
|
19
9
|
def aggressive(opts = {})
|
20
|
-
fingerprint(
|
10
|
+
fingerprint(DB::Fingerprints.wp_unique_fingerprints, opts) do |version_number, url, md5sum|
|
21
11
|
hydra.abort
|
22
12
|
progress_bar.finish
|
23
13
|
|
@@ -31,30 +21,6 @@ module WPScan
|
|
31
21
|
nil
|
32
22
|
end
|
33
23
|
|
34
|
-
# @return [ Hash ] The unique fingerprints across all versions in the DB
|
35
|
-
#
|
36
|
-
# Format returned:
|
37
|
-
# {
|
38
|
-
# file_path_1: {
|
39
|
-
# md5_hash_1: version_1,
|
40
|
-
# md5_hash_2: version_2
|
41
|
-
# },
|
42
|
-
# file_path_2: {
|
43
|
-
# md5_hash_3: version_1,
|
44
|
-
# md5_hash_4: version_3
|
45
|
-
# }
|
46
|
-
# }
|
47
|
-
def unique_fingerprints
|
48
|
-
fingerprints = {}
|
49
|
-
|
50
|
-
repository(:default).adapter.select(QUERY).each do |f|
|
51
|
-
fingerprints[f.path] ||= {}
|
52
|
-
fingerprints[f.path][f.md5_hash] = f.version
|
53
|
-
end
|
54
|
-
|
55
|
-
fingerprints
|
56
|
-
end
|
57
|
-
|
58
24
|
def create_progress_bar(opts = {})
|
59
25
|
super(opts.merge(title: 'Fingerprinting the version -'))
|
60
26
|
end
|
data/app/models/wp_item.rb
CHANGED
@@ -6,8 +6,8 @@ module WPScan
|
|
6
6
|
include CMSScanner::Target::Platform::PHP
|
7
7
|
include CMSScanner::Target::Server::Generic
|
8
8
|
|
9
|
-
READMES = %w[readme.txt README.txt
|
10
|
-
CHANGELOGS = %w[changelog.txt
|
9
|
+
READMES = %w[readme.txt README.txt README.md readme.md Readme.txt].freeze
|
10
|
+
CHANGELOGS = %w[changelog.txt CHANGELOG.md changelog.md].freeze
|
11
11
|
|
12
12
|
attr_reader :uri, :name, :detection_opts, :version_detection_opts, :target, :db_data
|
13
13
|
|
data/app/models/wp_version.rb
CHANGED
@@ -22,9 +22,15 @@ module WPScan
|
|
22
22
|
|
23
23
|
@all_numbers = []
|
24
24
|
|
25
|
-
DB::
|
25
|
+
DB::Fingerprints.wp_fingerprints.each_value do |fp|
|
26
|
+
fp.each_value do |versions|
|
27
|
+
versions.each do |version|
|
28
|
+
@all_numbers << version unless @all_numbers.include?(version)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
26
32
|
|
27
|
-
@all_numbers
|
33
|
+
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
|
28
34
|
end
|
29
35
|
|
30
36
|
# @return [ JSON ]
|
data/lib/wpscan.rb
CHANGED
data/lib/wpscan/db.rb
CHANGED
@@ -1,10 +1,4 @@
|
|
1
|
-
require 'dm-core'
|
2
|
-
require 'dm-migrations'
|
3
|
-
require 'dm-constraints'
|
4
|
-
require 'dm-sqlite-adapter'
|
5
|
-
|
6
1
|
require 'wpscan/db/wp_item'
|
7
|
-
require 'wpscan/db/schema'
|
8
2
|
require 'wpscan/db/updater'
|
9
3
|
require 'wpscan/db/wp_items'
|
10
4
|
require 'wpscan/db/plugins'
|
@@ -12,17 +6,5 @@ require 'wpscan/db/themes'
|
|
12
6
|
require 'wpscan/db/plugin'
|
13
7
|
require 'wpscan/db/theme'
|
14
8
|
require 'wpscan/db/wp_version'
|
9
|
+
require 'wpscan/db/fingerprints'
|
15
10
|
require 'wpscan/db/dynamic_finders'
|
16
|
-
|
17
|
-
module WPScan
|
18
|
-
# DB
|
19
|
-
module DB
|
20
|
-
def self.init_db
|
21
|
-
db_file ||= File.join(DB_DIR, 'wordpress.db')
|
22
|
-
|
23
|
-
# DataMapper::Logger.new($stdout, :debug)
|
24
|
-
DataMapper.setup(:default, "sqlite://#{db_file}")
|
25
|
-
DataMapper.auto_upgrade!
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
@@ -4,7 +4,7 @@ module WPScan
|
|
4
4
|
class DynamicFinders
|
5
5
|
# @return [ String ]
|
6
6
|
def self.db_file
|
7
|
-
@db_file ||= File.join(DB_DIR, '
|
7
|
+
@db_file ||= File.join(DB_DIR, 'dynamic_finders_01.yml')
|
8
8
|
end
|
9
9
|
|
10
10
|
# @return [ Hash ]
|
@@ -35,15 +35,7 @@ module WPScan
|
|
35
35
|
|
36
36
|
# @return [ Hash ]
|
37
37
|
def self.comments
|
38
|
-
|
39
|
-
@comments = finder_configs('Comments')
|
40
|
-
|
41
|
-
@comments.each do |slug, config|
|
42
|
-
@comments[slug]['pattern'] = Regexp.new(config['pattern'], Regexp::IGNORECASE)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
@comments
|
38
|
+
@comments ||= finder_configs('Comments')
|
47
39
|
end
|
48
40
|
|
49
41
|
# @return [ Hash ]
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module WPScan
|
2
|
+
module DB
|
3
|
+
# Fingerprints class
|
4
|
+
class Fingerprints
|
5
|
+
# @param [ Hash ] data
|
6
|
+
#
|
7
|
+
# @return [ Hash ] the unique fingerprints in the data argument given
|
8
|
+
# Format returned:
|
9
|
+
# {
|
10
|
+
# file_path_1: {
|
11
|
+
# md5_hash_1: version_1,
|
12
|
+
# md5_hash_2: version_2
|
13
|
+
# },
|
14
|
+
# file_path_2: {
|
15
|
+
# md5_hash_3: version_1,
|
16
|
+
# md5_hash_4: version_3
|
17
|
+
# }
|
18
|
+
# }
|
19
|
+
def self.unique_fingerprints(data)
|
20
|
+
unique_fingerprints = {}
|
21
|
+
|
22
|
+
data.each do |file_path, fingerprints|
|
23
|
+
fingerprints.each do |md5sum, versions|
|
24
|
+
next unless versions.size == 1
|
25
|
+
|
26
|
+
unique_fingerprints[file_path] ||= {}
|
27
|
+
unique_fingerprints[file_path][md5sum] = versions.first
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
unique_fingerprints
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [ String ]
|
35
|
+
def self.wp_fingerprints_path
|
36
|
+
@wp_unique_fingerprints_path ||= File.join(DB_DIR, 'wp_fingerprints.json')
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [ Hash ]
|
40
|
+
def self.wp_fingerprints
|
41
|
+
@wp_fingerprints ||= read_json_file(wp_fingerprints_path)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [ Hash ]
|
45
|
+
def self.wp_unique_fingerprints
|
46
|
+
@wp_unique_fingerprints ||= unique_fingerprints(wp_fingerprints)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/wpscan/db/updater.rb
CHANGED
@@ -7,9 +7,11 @@ module WPScan
|
|
7
7
|
FILES = %w[
|
8
8
|
plugins.json themes.json wordpresses.json
|
9
9
|
timthumbs-v3.txt user-agents.txt config_backups.txt
|
10
|
-
|
10
|
+
dynamic_finders_01.yml wp_fingerprints.json LICENSE
|
11
11
|
].freeze
|
12
12
|
|
13
|
+
OLD_FILES = %w[wordpress.db dynamic_finders.yml].freeze
|
14
|
+
|
13
15
|
attr_reader :repo_directory
|
14
16
|
|
15
17
|
def initialize(repo_directory)
|
@@ -18,6 +20,16 @@ module WPScan
|
|
18
20
|
FileUtils.mkdir_p(repo_directory) unless Dir.exist?(repo_directory)
|
19
21
|
|
20
22
|
raise "#{repo_directory} is not writable" unless Pathname.new(repo_directory).writable?
|
23
|
+
|
24
|
+
delete_old_files
|
25
|
+
end
|
26
|
+
|
27
|
+
# Removes DB files which are no longer used
|
28
|
+
# this doesn't raise errors if they don't exist
|
29
|
+
def delete_old_files
|
30
|
+
OLD_FILES.each do |old_file|
|
31
|
+
FileUtils.remove_file(local_file_path(old_file), true)
|
32
|
+
end
|
21
33
|
end
|
22
34
|
|
23
35
|
# @return [ Time, nil ]
|
data/lib/wpscan/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wpscan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.
|
4
|
+
version: 3.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- WPScanTeam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-10-
|
11
|
+
date: 2017-10-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cms_scanner
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.0.37.
|
19
|
+
version: 0.0.37.12
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.0.37.
|
26
|
+
version: 0.0.37.12
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: yajl-ruby
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,62 +52,6 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '5.1'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: dm-core
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 1.2.0
|
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.0
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: dm-migrations
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - "~>"
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: 1.2.0
|
76
|
-
type: :runtime
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - "~>"
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: 1.2.0
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: dm-constraints
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - "~>"
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: 1.2.0
|
90
|
-
type: :runtime
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - "~>"
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: 1.2.0
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: dm-sqlite-adapter
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - "~>"
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: 1.2.0
|
104
|
-
type: :runtime
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - "~>"
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: 1.2.0
|
111
55
|
- !ruby/object:Gem::Dependency
|
112
56
|
name: rake
|
113
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -128,14 +72,14 @@ dependencies:
|
|
128
72
|
requirements:
|
129
73
|
- - "~>"
|
130
74
|
- !ruby/object:Gem::Version
|
131
|
-
version: 3.
|
75
|
+
version: 3.7.0
|
132
76
|
type: :development
|
133
77
|
prerelease: false
|
134
78
|
version_requirements: !ruby/object:Gem::Requirement
|
135
79
|
requirements:
|
136
80
|
- - "~>"
|
137
81
|
- !ruby/object:Gem::Version
|
138
|
-
version: 3.
|
82
|
+
version: 3.7.0
|
139
83
|
- !ruby/object:Gem::Dependency
|
140
84
|
name: rspec-its
|
141
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -361,9 +305,9 @@ files:
|
|
361
305
|
- lib/wpscan/controllers.rb
|
362
306
|
- lib/wpscan/db.rb
|
363
307
|
- lib/wpscan/db/dynamic_finders.rb
|
308
|
+
- lib/wpscan/db/fingerprints.rb
|
364
309
|
- lib/wpscan/db/plugin.rb
|
365
310
|
- lib/wpscan/db/plugins.rb
|
366
|
-
- lib/wpscan/db/schema.rb
|
367
311
|
- lib/wpscan/db/theme.rb
|
368
312
|
- lib/wpscan/db/themes.rb
|
369
313
|
- lib/wpscan/db/updater.rb
|
data/lib/wpscan/db/schema.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
module WPScan
|
2
|
-
module DB
|
3
|
-
# WP Version
|
4
|
-
class Version < WpItem
|
5
|
-
include DataMapper::Resource
|
6
|
-
|
7
|
-
storage_names[:default] = 'versions'
|
8
|
-
|
9
|
-
has n, :fingerprints, constraint: :destroy
|
10
|
-
|
11
|
-
property :id, Serial
|
12
|
-
property :number, String, required: true, unique: true
|
13
|
-
end
|
14
|
-
|
15
|
-
# Path
|
16
|
-
class Path
|
17
|
-
include DataMapper::Resource
|
18
|
-
|
19
|
-
storage_names[:default] = 'paths'
|
20
|
-
|
21
|
-
has n, :fingerprints, constraint: :destroy
|
22
|
-
|
23
|
-
property :id, Serial
|
24
|
-
property :value, String, required: true, unique: true
|
25
|
-
end
|
26
|
-
|
27
|
-
# Fingerprint
|
28
|
-
class Fingerprint
|
29
|
-
include DataMapper::Resource
|
30
|
-
|
31
|
-
storage_names[:default] = 'fingerprints'
|
32
|
-
|
33
|
-
belongs_to :version, key: true
|
34
|
-
belongs_to :path, key: true
|
35
|
-
|
36
|
-
property :md5_hash, String, required: true, length: 32
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|