dradis-projects 3.0.0
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 +7 -0
- data/.gitignore +10 -0
- data/.ruby-version +1 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +17 -0
- data/LICENSE +339 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/app/controllers/dradis/plugins/projects/packages_controller.rb +17 -0
- data/app/controllers/dradis/plugins/projects/templates_controller.rb +16 -0
- data/config/routes.rb +4 -0
- data/dradis-projects.gemspec +30 -0
- data/lib/dradis-projects.rb +5 -0
- data/lib/dradis/plugins/projects.rb +15 -0
- data/lib/dradis/plugins/projects/engine.rb +31 -0
- data/lib/dradis/plugins/projects/export/package.rb +37 -0
- data/lib/dradis/plugins/projects/export/template.rb +173 -0
- data/lib/dradis/plugins/projects/gem_version.rb +19 -0
- data/lib/dradis/plugins/projects/upload/package.rb +71 -0
- data/lib/dradis/plugins/projects/upload/template.rb +364 -0
- data/lib/dradis/plugins/projects/version.rb +13 -0
- data/lib/tasks/thorfile.rb +139 -0
- metadata +150 -0
data/config/routes.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
$:.push File.expand_path('../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'dradis/plugins/projects/version'
|
4
|
+
|
5
|
+
# Describe your gem and declare its dependencies:
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.platform = Gem::Platform::RUBY
|
8
|
+
spec.name = 'dradis-projects'
|
9
|
+
spec.version = Dradis::Plugins::Projects::VERSION::STRING
|
10
|
+
spec.summary = 'Project export/upload for the Dradis Framework.'
|
11
|
+
spec.description = 'This plugin allows you to dump the contents of the repo into a zip archive and restore the state from one of them.'
|
12
|
+
|
13
|
+
spec.license = 'GPL-2'
|
14
|
+
|
15
|
+
spec.authors = ['Daniel Martin']
|
16
|
+
spec.email = ['etd@nomejortu.com']
|
17
|
+
spec.homepage = 'http://dradisframework.org'
|
18
|
+
|
19
|
+
spec.files = `git ls-files`.split($\)
|
20
|
+
spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
21
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.6'
|
24
|
+
spec.add_development_dependency 'combustion'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
26
|
+
spec.add_development_dependency 'rspec'
|
27
|
+
|
28
|
+
spec.add_dependency 'dradis-plugins', '~> 3.5'
|
29
|
+
spec.add_dependency 'rubyzip', '~> 1.1.0'
|
30
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Dradis
|
2
|
+
module Plugins
|
3
|
+
module Projects
|
4
|
+
module Export; end
|
5
|
+
module Upload; end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'dradis/plugins/projects/engine'
|
11
|
+
require 'dradis/plugins/projects/export/package'
|
12
|
+
require 'dradis/plugins/projects/export/template'
|
13
|
+
require 'dradis/plugins/projects/upload/package'
|
14
|
+
require 'dradis/plugins/projects/upload/template'
|
15
|
+
require 'dradis/plugins/projects/version'
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Dradis
|
2
|
+
module Plugins
|
3
|
+
module Projects
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace Dradis::Plugins::Projects
|
6
|
+
|
7
|
+
include ::Dradis::Plugins::Base
|
8
|
+
description 'Save and restore project information'
|
9
|
+
provides :export, :upload
|
10
|
+
|
11
|
+
initializer 'dradis-projects.mount_engine' do
|
12
|
+
Rails.application.routes.append do
|
13
|
+
mount Dradis::Plugins::Projects::Engine => '/export/projects'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Because this plugin provides two export modules, we have to overwrite
|
18
|
+
# the default .uploaders() method.
|
19
|
+
#
|
20
|
+
# See:
|
21
|
+
# Dradis::Plugins::Upload::Base in dradis-plugins
|
22
|
+
def self.uploaders
|
23
|
+
[
|
24
|
+
Dradis::Plugins::Projects::Upload::Package,
|
25
|
+
Dradis::Plugins::Projects::Upload::Template
|
26
|
+
]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Dradis::Plugins::Projects::Export
|
2
|
+
class Package < Dradis::Plugins::Export::Base
|
3
|
+
|
4
|
+
# Create a new project export bundle. It will include an XML file with the
|
5
|
+
# contents of the repository (see db_only) and all the attachments that
|
6
|
+
# have been uploaded into the system.
|
7
|
+
def export(params={})
|
8
|
+
raise ":filename not provided" unless params.key?(:filename)
|
9
|
+
|
10
|
+
filename = params[:filename]
|
11
|
+
logger = params.fetch(:logger, Rails.logger)
|
12
|
+
|
13
|
+
File.delete(filename) if File.exists?(filename)
|
14
|
+
|
15
|
+
logger.debug{ "Creating a new Zip file in #{filename}..." }
|
16
|
+
Zip::File.open(filename, Zip::File::CREATE) do |zipfile|
|
17
|
+
Node.all.each do |node|
|
18
|
+
node_path = Attachment.pwd.join(node.id.to_s)
|
19
|
+
|
20
|
+
Dir["#{node_path}/**/**"].each do |file|
|
21
|
+
logger.debug{ "\tAdding attachment for '#{node.label}': #{file}" }
|
22
|
+
zipfile.add(file.sub("#{Attachment.pwd.to_s}/", ''), file)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
logger.debug{ "\tAdding XML repository dump" }
|
27
|
+
template_exporter = Template.new(content_service: content_service)
|
28
|
+
template = template_exporter.export(params)
|
29
|
+
zipfile.get_output_stream('dradis-repository.xml') { |out|
|
30
|
+
out << template
|
31
|
+
}
|
32
|
+
end
|
33
|
+
logger.debug{ 'Done.' }
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module Dradis::Plugins::Projects::Export
|
2
|
+
class Template < Dradis::Plugins::Export::Base
|
3
|
+
# This method returns an XML representation of current repository which
|
4
|
+
# includes Categories, Nodes and Notes
|
5
|
+
def export(params={})
|
6
|
+
builder = Builder::XmlMarkup.new
|
7
|
+
builder.instruct!
|
8
|
+
result = builder.tag!('dradis-template') do |template_builder|
|
9
|
+
build_nodes(template_builder)
|
10
|
+
build_issues(template_builder)
|
11
|
+
build_methodologies(template_builder)
|
12
|
+
build_categories(template_builder)
|
13
|
+
build_tags(template_builder)
|
14
|
+
end
|
15
|
+
return result
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def build_activities_for(builder, trackable)
|
21
|
+
builder.activities do |activities_builder|
|
22
|
+
trackable.activities.each do |activity|
|
23
|
+
activities_builder.activity do |activity_builder|
|
24
|
+
activity_builder.action(activity.action)
|
25
|
+
activity_builder.user_email(user_email_for_activity(activity))
|
26
|
+
activity_builder.created_at(activity.created_at.to_i)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_categories(builder)
|
33
|
+
categories = []
|
34
|
+
categories << Category.issue if @issues.any?
|
35
|
+
categories += @nodes.map do |node|
|
36
|
+
node.notes.map { |note| note.category }.uniq
|
37
|
+
end.flatten.uniq
|
38
|
+
|
39
|
+
builder.categories do |categories_builder|
|
40
|
+
categories.each do |category|
|
41
|
+
categories_builder.category do |category_builder|
|
42
|
+
category_builder.id(category.id)
|
43
|
+
category_builder.name(category.name)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_evidence_for_node(builder, node)
|
50
|
+
builder.evidence do |evidences_builder|
|
51
|
+
node.evidence.each do |evidence|
|
52
|
+
evidences_builder.evidence do |evidence_builder|
|
53
|
+
evidence_builder.id(evidence.id)
|
54
|
+
evidence_builder.author(evidence.author)
|
55
|
+
evidence_builder.tag!('issue-id', evidence.issue_id)
|
56
|
+
evidence_builder.content do
|
57
|
+
evidence_builder.cdata!(evidence.content)
|
58
|
+
end
|
59
|
+
build_activities_for(evidence_builder, evidence)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def build_issues(builder)
|
66
|
+
@issues = Issue.where(node_id: Node.issue_library).includes(:activities)
|
67
|
+
|
68
|
+
builder.issues do |issues_builder|
|
69
|
+
@issues.each do |issue|
|
70
|
+
issues_builder.issue do |issue_builder|
|
71
|
+
issue_builder.id(issue.id)
|
72
|
+
issue_builder.author(issue.author)
|
73
|
+
issue_builder.text do
|
74
|
+
issue_builder.cdata!(issue.text)
|
75
|
+
end
|
76
|
+
build_activities_for(issue_builder, issue)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def build_methodologies(builder)
|
83
|
+
methodologies = Node.methodology_library.notes
|
84
|
+
builder.methodologies do |methodologies_builder|
|
85
|
+
methodologies.each do |methodology|
|
86
|
+
methodologies_builder.methodology do |methodology_builder|
|
87
|
+
methodology_builder.text do
|
88
|
+
methodology_builder.cdata!(methodology.text)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def build_nodes(builder)
|
96
|
+
@nodes = Node.includes(:activities, :evidence, :notes, evidence: [:activities], notes: [:activities, :category]).all.reject do |node|
|
97
|
+
[Node::Types::METHODOLOGY,
|
98
|
+
Node::Types::ISSUELIB].include?(node.type_id)
|
99
|
+
end
|
100
|
+
|
101
|
+
builder.nodes do |nodes_builder|
|
102
|
+
@nodes.each do |node|
|
103
|
+
nodes_builder.node do |node_builder|
|
104
|
+
node_builder.id(node.id)
|
105
|
+
node_builder.label(node.label)
|
106
|
+
node_builder.tag!('parent-id', node.parent_id)
|
107
|
+
node_builder.position(node.position)
|
108
|
+
node_builder.properties do
|
109
|
+
node_builder.cdata!(node.raw_properties)
|
110
|
+
end
|
111
|
+
node_builder.tag!('type-id', node.type_id)
|
112
|
+
# Notes
|
113
|
+
build_notes_for_node(node_builder, node)
|
114
|
+
# Evidence
|
115
|
+
build_evidence_for_node(node_builder, node)
|
116
|
+
build_activities_for(node_builder, node)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_notes_for_node(builder, node)
|
123
|
+
builder.notes do |notes_builder|
|
124
|
+
node.notes.each do |note|
|
125
|
+
notes_builder.note do |note_builder|
|
126
|
+
note_builder.id(note.id)
|
127
|
+
note_builder.author(note.author)
|
128
|
+
note_builder.tag!('category-id', note.category_id)
|
129
|
+
note_builder.text do
|
130
|
+
note_builder.cdata!(note.text)
|
131
|
+
end
|
132
|
+
build_activities_for(note_builder, note)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def build_tags(builder)
|
139
|
+
tags = Tag.all
|
140
|
+
builder.tags do |tags_builder|
|
141
|
+
tags.each do |tag|
|
142
|
+
tags_builder.tag do |tag_builder|
|
143
|
+
tag_builder.id(tag.id)
|
144
|
+
tag_builder.name(tag.name)
|
145
|
+
tag_builder.taggings do |taggings_builder|
|
146
|
+
tag.taggings.each do |tagging|
|
147
|
+
taggings_builder.tagging do |tagging_builder|
|
148
|
+
tagging_builder.tag!('taggable-id', tagging.taggable_id)
|
149
|
+
tagging_builder.tag!('taggable-type', tagging.taggable_type)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
# Cache user emails so we don't have to make an extra SQL request
|
160
|
+
# for every activity
|
161
|
+
def user_email_for_activity(activity)
|
162
|
+
return activity.user if activity.user.is_a?(String)
|
163
|
+
|
164
|
+
@user_emails ||= begin
|
165
|
+
User.select([:id, :email]).all.each_with_object({}) do |user, hash|
|
166
|
+
hash[user.id] = user.email
|
167
|
+
end
|
168
|
+
end
|
169
|
+
@user_emails[activity.user_id]
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dradis
|
2
|
+
module Plugins
|
3
|
+
module Projects
|
4
|
+
# Returns the version of the currently loaded Projects as a <tt>Gem::Version</tt>
|
5
|
+
def self.gem_version
|
6
|
+
Gem::Version.new VERSION::STRING
|
7
|
+
end
|
8
|
+
|
9
|
+
module VERSION
|
10
|
+
MAJOR = 3
|
11
|
+
MINOR = 0
|
12
|
+
TINY = 0
|
13
|
+
PRE = nil
|
14
|
+
|
15
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Dradis::Plugins::Projects::Upload
|
2
|
+
module Package
|
3
|
+
def self.meta
|
4
|
+
package = Dradis::Plugins::Projects
|
5
|
+
{
|
6
|
+
name: package::Engine::plugin_name,
|
7
|
+
description: 'Upload Project package file (.zip)',
|
8
|
+
version: package.version
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
# In this module you will find the implementation details that enable you to
|
13
|
+
# upload a project archive (generated using ProjectExport::Processor::full_project)
|
14
|
+
class Importer < Dradis::Plugins::Upload::Importer
|
15
|
+
|
16
|
+
def import(params={})
|
17
|
+
package = params[:file]
|
18
|
+
success = false
|
19
|
+
|
20
|
+
# Unpack the archive in a temporary location
|
21
|
+
FileUtils.mkdir Rails.root.join('tmp', 'zip')
|
22
|
+
|
23
|
+
begin
|
24
|
+
logger.info { 'Uncompressing the file' }
|
25
|
+
#TODO: this could be improved by only uncompressing the XML, then parsing
|
26
|
+
# it to get the node_lookup table and then uncompressing each entry to its
|
27
|
+
# final destination
|
28
|
+
Zip::File.foreach(package) do |entry|
|
29
|
+
path = Rails.root.join('tmp', 'zip', entry.name)
|
30
|
+
FileUtils.mkdir_p(File.dirname(path))
|
31
|
+
entry.extract(path)
|
32
|
+
logger.info { "\t#{entry.name}" }
|
33
|
+
end
|
34
|
+
logger.info { 'Done.' }
|
35
|
+
|
36
|
+
logger.info { 'Loading XML state file' }
|
37
|
+
importer = Template::Importer.new(
|
38
|
+
content_service: content_service,
|
39
|
+
logger: logger,
|
40
|
+
template_service: template_service
|
41
|
+
)
|
42
|
+
node_lookup = importer.import(
|
43
|
+
file: Rails.root.
|
44
|
+
join('tmp', 'zip', 'dradis-repository.xml')
|
45
|
+
)
|
46
|
+
|
47
|
+
logger.info { 'Moving attachments to their final destinations' }
|
48
|
+
node_lookup.each do |oldid,newid|
|
49
|
+
if File.directory? Rails.root.join('tmp', 'zip', oldid)
|
50
|
+
FileUtils.mkdir_p Attachment.pwd.join(newid.to_s)
|
51
|
+
|
52
|
+
Dir.glob(Rails.root.join('tmp', 'zip', oldid, '*')).each do |attachment|
|
53
|
+
FileUtils.mv(attachment, Attachment.pwd.join(newid.to_s))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
success = true
|
59
|
+
rescue Exception => e
|
60
|
+
logger.error { e.message }
|
61
|
+
success = false
|
62
|
+
ensure
|
63
|
+
# clean up the temporary files
|
64
|
+
FileUtils.rm_rf(Rails.root.join('tmp', 'zip'))
|
65
|
+
end
|
66
|
+
|
67
|
+
return success
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,364 @@
|
|
1
|
+
module Dradis::Plugins::Projects::Upload
|
2
|
+
module Template
|
3
|
+
def self.meta
|
4
|
+
package = Dradis::Plugins::Projects
|
5
|
+
{
|
6
|
+
name: package::Engine::plugin_name,
|
7
|
+
description: 'Upload Project template file (.xml)',
|
8
|
+
version: package.version
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
class Importer < Dradis::Plugins::Upload::Importer
|
13
|
+
|
14
|
+
# The import method is invoked by the framework to process a template file
|
15
|
+
# that has just been uploaded using the 'Import from file...' dialog.
|
16
|
+
#
|
17
|
+
# This module will take the XMl export file created with the ProjectExport
|
18
|
+
# module and dump the contents into the current database.
|
19
|
+
#
|
20
|
+
# Since we cannot ensure that the original node and category IDs as specified
|
21
|
+
# in the XML are free in this database, we need to keep a few lookup tables
|
22
|
+
# to maintain the original structure of Nodes and the Notes pointing to the
|
23
|
+
# right nodes and categories.
|
24
|
+
#
|
25
|
+
# This method also returns the Node lookup table so callers can understand
|
26
|
+
# what changes to the original IDs have been applied. This is mainly for the
|
27
|
+
# benefit of the ProjectPackageUpload module that would use the translation
|
28
|
+
# table to re-associate the attachments in the project archive with the new
|
29
|
+
# node IDs in the current project.
|
30
|
+
def import(params={})
|
31
|
+
|
32
|
+
# load the template
|
33
|
+
logger.info { "Loading template file from: #{params[:file]}" }
|
34
|
+
template = Nokogiri::XML(File.read(params[:file]))
|
35
|
+
logger.info { "Done." }
|
36
|
+
|
37
|
+
unless template.errors.empty?
|
38
|
+
logger.error { "Invalid project template format." }
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
|
42
|
+
# we need this to be able to convert from old category_id to the new
|
43
|
+
# category_id once the categories are added to the DB (the ID may have
|
44
|
+
# changed)
|
45
|
+
category_lookup = {}
|
46
|
+
# the same applies to Nodes (think parent_id)
|
47
|
+
node_lookup = {}
|
48
|
+
# and to issues
|
49
|
+
issue_lookup = {}
|
50
|
+
|
51
|
+
# evidence is parsed when nodes are parsed, but cannot be saved until issues
|
52
|
+
# have been created. Therefore, parse evidence into arrays until time for
|
53
|
+
# creation
|
54
|
+
evidence_array = []
|
55
|
+
|
56
|
+
# likewise we also need to hold on to the XML about evidence activities
|
57
|
+
# until after the evidence has been saved
|
58
|
+
evidence_activity_xml_array = []
|
59
|
+
|
60
|
+
# all children nodes, we will need to find the new ID of their parents
|
61
|
+
orphan_nodes = []
|
62
|
+
|
63
|
+
# if the note has an attachment screenshot (i.e. !.../nodes/i/attachments/...!)
|
64
|
+
# we will fix the URL to point to the new Node ID.
|
65
|
+
#
|
66
|
+
# WARNING: we need a lookup table because one note may be referencing a
|
67
|
+
# different (yet unprocessed) node's attachments.
|
68
|
+
attachment_notes = []
|
69
|
+
|
70
|
+
# go through the categories, keep a translation table between the old
|
71
|
+
# category id and the new ones so we know to which category we should
|
72
|
+
# assign our notes
|
73
|
+
template.xpath('dradis-template/categories/category').each do |xml_category|
|
74
|
+
old_id = xml_category.at_xpath('id').text.strip
|
75
|
+
name = xml_category.at_xpath('name').text.strip
|
76
|
+
category = nil
|
77
|
+
|
78
|
+
# Prevent creating duplicate categories
|
79
|
+
logger.info { "Looking for category: #{name}" }
|
80
|
+
category = Category.find_or_create_by!(name: name)
|
81
|
+
category_lookup[old_id] = category.id
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# ------------------------------------------------------------------- Nodes
|
86
|
+
# Re generate the Node tree structure
|
87
|
+
template.xpath('dradis-template/nodes/node').each do |xml_node|
|
88
|
+
element = xml_node.at_xpath('type-id')
|
89
|
+
type_id = element.text.nil? ? nil : element.text.strip
|
90
|
+
label = xml_node.at_xpath('label').text.strip
|
91
|
+
element = xml_node.at_xpath('parent-id')
|
92
|
+
parent_id = element.text.nil? ? nil : element.text.strip
|
93
|
+
|
94
|
+
# Node positions
|
95
|
+
element = xml_node.at_xpath('position')
|
96
|
+
position = (element && !element.text.nil?) ? element.text.strip : nil
|
97
|
+
|
98
|
+
# Node properties
|
99
|
+
element = xml_node.at_xpath('properties')
|
100
|
+
properties = (element && !element.text.blank?) ? element.text.strip : nil
|
101
|
+
|
102
|
+
created_at = xml_node.at_xpath('created-at')
|
103
|
+
updated_at = xml_node.at_xpath('updated-at')
|
104
|
+
|
105
|
+
logger.info { "New node detected: #{label}, parent_id: #{parent_id}, type_id: #{type_id}" }
|
106
|
+
|
107
|
+
# There is one exception to the rule, the Configuration.uploadsNode node,
|
108
|
+
# it does not make sense to have more than one of this nodes, in any
|
109
|
+
# given tree
|
110
|
+
node = nil
|
111
|
+
note = nil
|
112
|
+
evidence = nil
|
113
|
+
if (label == Configuration.plugin_uploads_node)
|
114
|
+
node = Node.create_with(type_id: type_id, parent_id: parent_id).
|
115
|
+
find_or_create_by!(label: label)
|
116
|
+
else
|
117
|
+
node = Node.create!(
|
118
|
+
type_id: type_id,
|
119
|
+
label: label,
|
120
|
+
parent_id: parent_id,
|
121
|
+
position: position
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
if properties
|
126
|
+
node.raw_properties = properties
|
127
|
+
end
|
128
|
+
|
129
|
+
node.update_attribute(:created_at, created_at.text.strip) if created_at
|
130
|
+
node.update_attribute(:updated_at, updated_at.text.strip) if updated_at
|
131
|
+
|
132
|
+
return false unless validate_and_save(node)
|
133
|
+
return false unless create_activities(node, xml_node)
|
134
|
+
|
135
|
+
xml_node.xpath('notes/note').each do |xml_note|
|
136
|
+
|
137
|
+
if xml_note.at_xpath('author') != nil
|
138
|
+
old_id = xml_note.at_xpath('category-id').text.strip
|
139
|
+
new_id = category_lookup[old_id]
|
140
|
+
|
141
|
+
created_at = xml_note.at_xpath('created-at')
|
142
|
+
updated_at = xml_note.at_xpath('updated-at')
|
143
|
+
|
144
|
+
logger.info { "Note category rewrite, used to be #{old_id}, now is #{new_id}" }
|
145
|
+
note = Note.create!(
|
146
|
+
author: xml_note.at_xpath('author').text.strip,
|
147
|
+
node_id: node.id,
|
148
|
+
category_id: new_id,
|
149
|
+
text: xml_note.at_xpath('text').text
|
150
|
+
)
|
151
|
+
|
152
|
+
note.update_attribute(:created_at, created_at.text.strip) if created_at
|
153
|
+
note.update_attribute(:updated_at, updated_at.text.strip) if updated_at
|
154
|
+
|
155
|
+
return false unless validate_and_save(note)
|
156
|
+
|
157
|
+
if note.text =~ %r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}
|
158
|
+
attachment_notes << note
|
159
|
+
end
|
160
|
+
|
161
|
+
return false unless create_activities(note, xml_note)
|
162
|
+
|
163
|
+
logger.info { "\tNew note added detected." }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Create array of evidence from xml input. Cannot store in DB until we
|
168
|
+
# have a new issue id
|
169
|
+
xml_node.xpath('evidence/evidence').each do |xml_evidence|
|
170
|
+
if xml_evidence.at_xpath('author') != nil
|
171
|
+
created_at = xml_evidence.at_xpath('created-at')
|
172
|
+
updated_at = xml_evidence.at_xpath('updated-at')
|
173
|
+
|
174
|
+
evidence = Evidence.new(
|
175
|
+
author: xml_evidence.at_xpath('author').text.strip,
|
176
|
+
node_id: node.id,
|
177
|
+
content: xml_evidence.at_xpath('content').text,
|
178
|
+
issue_id: xml_evidence.at_xpath('issue-id').text.strip
|
179
|
+
)
|
180
|
+
|
181
|
+
evidence.update_attribute(:created_at, created_at.text.strip) if created_at
|
182
|
+
evidence.update_attribute(:updated_at, updated_at.text.strip) if updated_at
|
183
|
+
evidence_array << evidence
|
184
|
+
|
185
|
+
evidence_activity_xml_array << xml_evidence.xpath("activities/activity")
|
186
|
+
|
187
|
+
logger.info { "\tNew evidence added." }
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# keep track of reassigned ids
|
192
|
+
node_lookup[xml_node.at_xpath('id').text.strip] = node.id
|
193
|
+
|
194
|
+
if node.parent_id != nil
|
195
|
+
# keep track of orphaned nodes
|
196
|
+
orphan_nodes << node
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
# ------------------------------------------------------------------- Issues
|
202
|
+
issue = nil
|
203
|
+
issue_category = Category.issue
|
204
|
+
issue_library = Node.issue_library
|
205
|
+
# go through the issues, keep a translation table between the old
|
206
|
+
# issue id and the new ones. This is important for importing evidence
|
207
|
+
# Will need to adjust node ID after generating node structure
|
208
|
+
template.xpath('dradis-template/issues/issue').each do |xml_issue|
|
209
|
+
old_id = xml_issue.at_xpath('id').text.strip
|
210
|
+
|
211
|
+
# TODO: Need to find some way of checking for dups
|
212
|
+
# May be combination of text, category_id and created_at
|
213
|
+
issue = Issue.new
|
214
|
+
issue.author = xml_issue.at_xpath('author').text.strip
|
215
|
+
issue.text = xml_issue.at_xpath('text').text
|
216
|
+
issue.node = issue_library
|
217
|
+
issue.category = issue_category
|
218
|
+
|
219
|
+
return false unless validate_and_save(issue)
|
220
|
+
|
221
|
+
return false unless create_activities(issue, xml_issue)
|
222
|
+
|
223
|
+
if issue.text =~ %r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}
|
224
|
+
attachment_notes << issue
|
225
|
+
end
|
226
|
+
|
227
|
+
issue_lookup[old_id] = issue.id
|
228
|
+
logger.info{ "New issue detected: #{issue.title}" }
|
229
|
+
end
|
230
|
+
|
231
|
+
# ----------------------------------------------------------- Methodologies
|
232
|
+
methodology_category = Category.default
|
233
|
+
methodology_library = Node.methodology_library
|
234
|
+
template.xpath('dradis-template/methodologies/methodology').each do |xml_methodology|
|
235
|
+
# FIXME: this is wrong in a few levels, we should be able to save a
|
236
|
+
# Methodology instance calling .save() but the current implementation
|
237
|
+
# of the model would consider this a 'methodology template' and not an
|
238
|
+
# instance.
|
239
|
+
#
|
240
|
+
# Also, methodology notes don't have a valid author, see
|
241
|
+
# MethodologiesController#create action (i.e. 'methodology builder' is
|
242
|
+
# used).
|
243
|
+
Note.create!(
|
244
|
+
author: 'methodology importer',
|
245
|
+
node_id: methodology_library.id,
|
246
|
+
category_id: methodology_category.id,
|
247
|
+
text: xml_methodology.at_xpath('text').text
|
248
|
+
)
|
249
|
+
end
|
250
|
+
|
251
|
+
# -------------------------------------------------------------------- Tags
|
252
|
+
template.xpath('dradis-template/tags/tag').each do |xml_tag|
|
253
|
+
name = xml_tag.at_xpath('name').text()
|
254
|
+
tag = Tag.find_or_create_by!(name: name)
|
255
|
+
@logger.info { "New tag detected: #{name}" }
|
256
|
+
|
257
|
+
xml_tag.xpath('./taggings/tagging').each do |xml_tagging|
|
258
|
+
old_taggable_id = xml_tagging.at_xpath('taggable-id').text()
|
259
|
+
taggable_type = xml_tagging.at_xpath('taggable-type').text()
|
260
|
+
|
261
|
+
new_taggable_id = case taggable_type
|
262
|
+
when 'Note'
|
263
|
+
issue_lookup[old_taggable_id]
|
264
|
+
end
|
265
|
+
|
266
|
+
Tagging.create! tag: tag, taggable_id: new_taggable_id, taggable_type: taggable_type
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# ----------------------------------------------------------------- Wrap up
|
271
|
+
|
272
|
+
logger.info { "Wrapping up..." }
|
273
|
+
|
274
|
+
# Save the Evidence instance to the DB now that we have populated Issues
|
275
|
+
# the original issues
|
276
|
+
evidence_array.each_with_index do |evidence, i|
|
277
|
+
logger.info { "Setting issue_id for evidence" }
|
278
|
+
evidence.issue_id = issue_lookup[evidence.issue_id.to_s]
|
279
|
+
|
280
|
+
new_content = evidence.content.gsub(%r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}) do |_|
|
281
|
+
"!%s/nodes/%d/attachments/%s!" % [$1, node_lookup[$2], $3]
|
282
|
+
end
|
283
|
+
evidence.content = new_content
|
284
|
+
|
285
|
+
return false unless validate_and_save(evidence)
|
286
|
+
|
287
|
+
evidence_activity_xml_array[i].each do |xml_activity|
|
288
|
+
return false unless create_activity(evidence, xml_activity)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Fix relationships between nodes to ensure parents and childrens match
|
293
|
+
# with the new assigned :ids
|
294
|
+
orphan_nodes.each do |node|
|
295
|
+
logger.info { "Finding parent for orphaned node: #{node.label}. Former parent was #{node.parent_id}" }
|
296
|
+
node.parent_id = node_lookup[node.parent_id.to_s]
|
297
|
+
return false unless validate_and_save(node)
|
298
|
+
end
|
299
|
+
|
300
|
+
# Adjust attachment URLs for new Node IDs
|
301
|
+
attachment_notes.each do |note|
|
302
|
+
@logger.info{ "Adjusting screenshot URLs: Note ##{note.id}" }
|
303
|
+
new_text = note.text.gsub(%r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}) do |_|
|
304
|
+
"!%s/nodes/%d/attachments/%s!" % [$1, node_lookup[$2], $3]
|
305
|
+
end
|
306
|
+
note.text = new_text
|
307
|
+
return false unless validate_and_save(note)
|
308
|
+
end
|
309
|
+
|
310
|
+
return node_lookup
|
311
|
+
end
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
def create_activities(trackable, xml_trackable)
|
316
|
+
xml_trackable.xpath('activities/activity').each do |xml_activity|
|
317
|
+
# if 'validate_and_save(activity)' returns false, it needs
|
318
|
+
# to bubble up to the 'import' method so we can stop execution
|
319
|
+
return false unless create_activity(trackable, xml_activity)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
def create_activity(trackable, xml_activity)
|
324
|
+
activity = trackable.activities.new(
|
325
|
+
action: xml_activity.at_xpath("action").text,
|
326
|
+
created_at: Time.at(xml_activity.at_xpath("created_at").text.to_i)
|
327
|
+
)
|
328
|
+
|
329
|
+
set_activity_user(activity, xml_activity.at_xpath("user_email").text)
|
330
|
+
|
331
|
+
validate_and_save(activity)
|
332
|
+
end
|
333
|
+
|
334
|
+
def set_activity_user(activity, email)
|
335
|
+
if Activity.column_names.include?('user')
|
336
|
+
activity.user = email
|
337
|
+
else
|
338
|
+
activity.user_id = user_id_for_email(email)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# Cache users to cut down on excess SQL requests
|
343
|
+
def user_id_for_email(email)
|
344
|
+
return -1 if email.blank?
|
345
|
+
@users ||= begin
|
346
|
+
User.select([:id, :email]).all.each_with_object({}) do |user, hash|
|
347
|
+
hash[user.email] = user.id
|
348
|
+
end
|
349
|
+
end
|
350
|
+
@users[email] || -1
|
351
|
+
end
|
352
|
+
|
353
|
+
def validate_and_save(instance)
|
354
|
+
if instance.save
|
355
|
+
return true
|
356
|
+
else
|
357
|
+
@logger.info{ "Malformed #{ instance.class.name } detected: #{ instance.errors.full_messages }" }
|
358
|
+
return false
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|