owasp-glue 0.9.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/CHANGES +27 -0
- data/FEATURES +19 -0
- data/README.md +117 -0
- data/bin/glue +67 -0
- data/lib/glue.rb +317 -0
- data/lib/glue/event.rb +14 -0
- data/lib/glue/filters.rb +41 -0
- data/lib/glue/filters/base_filter.rb +19 -0
- data/lib/glue/filters/jira_one_time_filter.rb +57 -0
- data/lib/glue/filters/remove_all_filter.rb +16 -0
- data/lib/glue/filters/zap_consdensing_filter.rb +76 -0
- data/lib/glue/finding.rb +52 -0
- data/lib/glue/mounters.rb +55 -0
- data/lib/glue/mounters/base_mounter.rb +31 -0
- data/lib/glue/mounters/docker_mounter.rb +44 -0
- data/lib/glue/mounters/filesystem_mounter.rb +20 -0
- data/lib/glue/mounters/git_mounter.rb +52 -0
- data/lib/glue/mounters/iso_mounter.rb +42 -0
- data/lib/glue/mounters/url_mounter.rb +28 -0
- data/lib/glue/options.rb +269 -0
- data/lib/glue/reporters.rb +50 -0
- data/lib/glue/reporters/base_reporter.rb +21 -0
- data/lib/glue/reporters/csv_reporter.rb +19 -0
- data/lib/glue/reporters/jira_reporter.rb +59 -0
- data/lib/glue/reporters/json_reporter.rb +20 -0
- data/lib/glue/reporters/text_reporter.rb +19 -0
- data/lib/glue/scanner.rb +28 -0
- data/lib/glue/tasks.rb +124 -0
- data/lib/glue/tasks/av.rb +42 -0
- data/lib/glue/tasks/base_task.rb +80 -0
- data/lib/glue/tasks/brakeman.rb +58 -0
- data/lib/glue/tasks/bundle-audit.rb +95 -0
- data/lib/glue/tasks/checkmarx.rb +60 -0
- data/lib/glue/tasks/dawnscanner.rb +55 -0
- data/lib/glue/tasks/eslint.rb +69 -0
- data/lib/glue/tasks/fim.rb +60 -0
- data/lib/glue/tasks/findsecbugs.rb +90 -0
- data/lib/glue/tasks/npm.rb +58 -0
- data/lib/glue/tasks/nsp.rb +65 -0
- data/lib/glue/tasks/owasp-dep-check.rb +117 -0
- data/lib/glue/tasks/patterns.json +394 -0
- data/lib/glue/tasks/pmd.rb +63 -0
- data/lib/glue/tasks/retirejs.rb +107 -0
- data/lib/glue/tasks/scanjs-eslintrc +106 -0
- data/lib/glue/tasks/scanjs.rb +31 -0
- data/lib/glue/tasks/sfl.rb +67 -0
- data/lib/glue/tasks/snyk.rb +81 -0
- data/lib/glue/tasks/test.rb +47 -0
- data/lib/glue/tasks/zap.rb +99 -0
- data/lib/glue/tracker.rb +47 -0
- data/lib/glue/util.rb +36 -0
- data/lib/glue/version.rb +3 -0
- metadata +294 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'glue/mounters/base_mounter'
|
2
|
+
|
3
|
+
class Glue::ISOMounter < Glue::BaseMounter
|
4
|
+
|
5
|
+
# THIS DOESN'T WORK SO DON'T REGISTER FOR NOW
|
6
|
+
# Glue::Mounters.add self
|
7
|
+
|
8
|
+
def initialize trigger, options
|
9
|
+
super(trigger)
|
10
|
+
@options = options
|
11
|
+
@name = "ISO"
|
12
|
+
@description = "Mount an iso image."
|
13
|
+
end
|
14
|
+
|
15
|
+
def mount target
|
16
|
+
base = @options[:working_dir]
|
17
|
+
working_target = base + "/" + target + "/"
|
18
|
+
Glue.notify "Cleaning directory: #{working_target}"
|
19
|
+
|
20
|
+
if ! working_target.match(/\A.*\/line\/tmp\/.*/)
|
21
|
+
Glue.notify "Bailing in case #{working_target} is malicious."
|
22
|
+
else
|
23
|
+
result = `rm -rf #{working_target}`
|
24
|
+
# puts result
|
25
|
+
result = `mkdir -p #{working_target}`
|
26
|
+
# puts result
|
27
|
+
Glue.notify "Mounting #{target} to #{working_target}"
|
28
|
+
result = `mount -t iso9660 #{target} #{working_target}`
|
29
|
+
# puts result
|
30
|
+
end
|
31
|
+
return working_target
|
32
|
+
end
|
33
|
+
|
34
|
+
def supports? target
|
35
|
+
last = target.slice(-4,target.length)
|
36
|
+
if last === ".iso"
|
37
|
+
return true
|
38
|
+
else
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'glue/mounters/base_mounter'
|
2
|
+
|
3
|
+
class Glue::URLMounter < Glue::BaseMounter
|
4
|
+
Glue::Mounters.add self
|
5
|
+
|
6
|
+
def initialize trigger, options
|
7
|
+
super(trigger)
|
8
|
+
@options = options
|
9
|
+
@name = "URL"
|
10
|
+
@description = "Mount a url - typically for a live attack."
|
11
|
+
end
|
12
|
+
|
13
|
+
def mount target
|
14
|
+
return target
|
15
|
+
end
|
16
|
+
|
17
|
+
def supports? target
|
18
|
+
start = target.slice(0,4)
|
19
|
+
last = target.slice(-4,target.length)
|
20
|
+
if last === ".git"
|
21
|
+
return false
|
22
|
+
elsif start === "http"
|
23
|
+
return true
|
24
|
+
else
|
25
|
+
return false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/glue/options.rb
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
#Parses command line arguments for Brakeman
|
5
|
+
module Glue::Options
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
#Parse argument array
|
10
|
+
def parse args
|
11
|
+
get_options args
|
12
|
+
end
|
13
|
+
|
14
|
+
#Parse arguments and remove them from the array as they are matched
|
15
|
+
def parse! args
|
16
|
+
get_options args, true
|
17
|
+
end
|
18
|
+
|
19
|
+
#Return hash of options and the parser
|
20
|
+
def get_options args, destructive = false
|
21
|
+
options = {}
|
22
|
+
|
23
|
+
parser = OptionParser.new do |opts|
|
24
|
+
opts.banner = "Usage: Glue [options] image/root/path"
|
25
|
+
|
26
|
+
opts.separator ""
|
27
|
+
opts.separator "Glue is a swiss army knife of security analysis tools."
|
28
|
+
opts.separator "It has built in support for static analysis, AV, fim, and "
|
29
|
+
opts.separator "is being extended to be used for analyzing all kinds of "
|
30
|
+
opts.separator "projects, images or file systems."
|
31
|
+
opts.separator ""
|
32
|
+
opts.separator "Glue also features filters to perform deduplication "
|
33
|
+
opts.separator "and the abilty to handle false positives."
|
34
|
+
opts.separator ""
|
35
|
+
opts.separator "See also the docker image."
|
36
|
+
opts.separator ""
|
37
|
+
|
38
|
+
opts.separator "Control options:"
|
39
|
+
|
40
|
+
opts.on "-n", "--no-threads", "Run checks sequentially" do
|
41
|
+
options[:parallel_checks] = false
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on "--[no-]progress", "Show progress reports" do |progress|
|
45
|
+
options[:report_progress] = progress
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on "-T", "--target PATH", "Specify target" do |target|
|
49
|
+
options[:target] = path
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on "-q", "--[no-]quiet", "Suppress informational messages" do |quiet|
|
53
|
+
options[:quiet] = quiet
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on( "-z", "--exit-on-warn", "Exit code is non-zero if warnings found") do
|
57
|
+
options[:exit_on_warn] = true
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.separator ""
|
61
|
+
opts.separator "Scanning options:"
|
62
|
+
|
63
|
+
opts.on "-A", "--run-all-checks", "Run all default and optional checks" do
|
64
|
+
options[:run_all_checks] = true
|
65
|
+
end
|
66
|
+
|
67
|
+
opts.on "-t", "--test Check1,Check2,etc", Array, "Only run the specified checks" do |checks|
|
68
|
+
options[:run_tasks] ||= Set.new
|
69
|
+
options[:run_tasks].merge checks
|
70
|
+
end
|
71
|
+
|
72
|
+
opts.on "-x", "--except Check1,Check2,etc", Array, "Skip the specified checks" do |skip|
|
73
|
+
skip.each do |s|
|
74
|
+
|
75
|
+
options[:skip_checks] ||= Set.new
|
76
|
+
options[:skip_checks] << s
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
opts.on "-a", "--appname NAME", "Override the inferred application name." do |appname|
|
81
|
+
options[:appname] = appname
|
82
|
+
end
|
83
|
+
|
84
|
+
opts.on "-r", "--revision REV", "Specify a revision of software to pass on to checkmarx" do |revision|
|
85
|
+
options[:revision] = revision
|
86
|
+
end
|
87
|
+
|
88
|
+
opts.on "-l", "--labels Label1,Label2,etc", Array, "Run the checks with the supplied labels" do |labels|
|
89
|
+
options[:labels] ||= Set.new
|
90
|
+
options[:labels].merge labels
|
91
|
+
end
|
92
|
+
|
93
|
+
opts.on "--add-checks-path path1,path2,etc", Array, "A directory containing additional out-of-tree checks to run" do |paths|
|
94
|
+
options[:additional_checks_path] ||= Set.new
|
95
|
+
options[:additional_checks_path].merge paths.map {|p| File.expand_path p}
|
96
|
+
end
|
97
|
+
|
98
|
+
opts.on "--npm-registry URL", "Use a custom npm registry when installing dependencies for javascript scanners" do |url|
|
99
|
+
options[:npm_registry] = url
|
100
|
+
end
|
101
|
+
|
102
|
+
opts.on "--exclude path1,path2,path3,etc", Array, "A list of paths to ignore when running recursive tasks (npm, retirejs, snyk, etc)" do |paths|
|
103
|
+
paths.each do |path|
|
104
|
+
options[:exclude_dirs] ||= Set.new
|
105
|
+
options[:exclude_dirs] << path
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
opts.separator ""
|
110
|
+
opts.separator "Output options:"
|
111
|
+
|
112
|
+
opts.on "-d", "--debug", "Lots of output" do
|
113
|
+
options[:debug] = true
|
114
|
+
end
|
115
|
+
|
116
|
+
opts.on "-f",
|
117
|
+
"--format TYPE",
|
118
|
+
[:text, :html, :csv, :tabs, :json, :jira, :markdown],
|
119
|
+
"Specify output formats. Default is text" do |type|
|
120
|
+
options[:output_format] = type
|
121
|
+
end
|
122
|
+
|
123
|
+
opts.on "--css-file CSSFile", "Specify CSS to use for HTML output" do |file|
|
124
|
+
options[:html_style] = File.expand_path file
|
125
|
+
end
|
126
|
+
|
127
|
+
opts.on "-i IGNOREFILE", "--ignore-config IGNOREFILE", "Use configuration to ignore warnings" do |file|
|
128
|
+
options[:ignore_file] = file
|
129
|
+
end
|
130
|
+
|
131
|
+
opts.on "-I", "--interactive-ignore", "Interactively ignore warnings" do
|
132
|
+
options[:interactive_ignore] = true
|
133
|
+
end
|
134
|
+
|
135
|
+
opts.on "-o", "--output FILE", "Specify file for output. Defaults to stdout." do |file|
|
136
|
+
options[:output_file] = file
|
137
|
+
end
|
138
|
+
|
139
|
+
opts.on "--summary", "Only output summary of warnings" do
|
140
|
+
options[:summary_only] = true
|
141
|
+
end
|
142
|
+
|
143
|
+
opts.on "-L LOGFILE", "--logfile LOGFILE", "Write full Glue log to LOGFILE" do |file|
|
144
|
+
options[:logfile] = file
|
145
|
+
end
|
146
|
+
|
147
|
+
opts.separator ""
|
148
|
+
opts.separator "JIRA options:"
|
149
|
+
|
150
|
+
opts.on "--jira-project PROJECT", "Specify the jira project to create issues in. If issue looks like APPS-13, this should be APPS." do |project|
|
151
|
+
options[:jira_project] = project
|
152
|
+
end
|
153
|
+
|
154
|
+
opts.on "--jira-api-url URL", "Specify the jira rest api endpoint. Eg. domain.com/jira/jira/rest/api/2/." do |url|
|
155
|
+
options[:jira_api_url] = url
|
156
|
+
end
|
157
|
+
|
158
|
+
opts.on "--jira-cookie COOKIE", "Specify the session cookie to get to Jira." do |cookie|
|
159
|
+
options[:jira_cookie] = cookie
|
160
|
+
end
|
161
|
+
|
162
|
+
opts.on "--jira-component COMPONENT", "Specify the JIRA component to use." do |component|
|
163
|
+
options[:jira_component] = component
|
164
|
+
end
|
165
|
+
|
166
|
+
opts.separator ""
|
167
|
+
opts.separator "ZAP options:"
|
168
|
+
|
169
|
+
opts.on "--zap-api-token token", "Specify the ZAP API token to use when connecting to the API" do |token|
|
170
|
+
options[:zap_api_token] = token
|
171
|
+
end
|
172
|
+
|
173
|
+
opts.on "--zap-host HOST", "Specify the host ZAP is running on." do |host|
|
174
|
+
options[:zap_host] = host
|
175
|
+
end
|
176
|
+
|
177
|
+
opts.on "--zap-port PORT", "Specify the port ZAP is running on." do |port|
|
178
|
+
options[:zap_port] = port
|
179
|
+
end
|
180
|
+
|
181
|
+
opts.separator ""
|
182
|
+
opts.separator "Checkmarx options:"
|
183
|
+
|
184
|
+
opts.on "--checkmarx-user USER", "Specify the Checkmarx user to use when connecting to the API" do |user|
|
185
|
+
options[:checkmarx_user] = user
|
186
|
+
end
|
187
|
+
|
188
|
+
opts.on "--checkmarx-password PASSWORD", "Specify password for the Checkmarx API user" do |password|
|
189
|
+
options[:checkmarx_password] = password
|
190
|
+
end
|
191
|
+
|
192
|
+
opts.on "--checkmarx-server server", "Specify the API server to use for Checkmarx scans" do |server|
|
193
|
+
options[:checkmarx_server] = server
|
194
|
+
end
|
195
|
+
|
196
|
+
opts.on "--checkmarx-log logfile", "Specify the log file to use for Checkmarx scans" do |logfile|
|
197
|
+
options[:checkmarx_log] = logfile
|
198
|
+
end
|
199
|
+
|
200
|
+
opts.on "--checkmarx-project project", "Specify the full path of the Checkmarx project for this scan" do |project|
|
201
|
+
options[:checkmarx_project] = project
|
202
|
+
end
|
203
|
+
|
204
|
+
opts.separator ""
|
205
|
+
opts.separator "PMD options:"
|
206
|
+
|
207
|
+
opts.on "--pmd-path PATH", "The full path to the base PMD directory" do |dir|
|
208
|
+
options[:pmd_path] = dir
|
209
|
+
end
|
210
|
+
|
211
|
+
opts.on "--pmd-checks CHECK1,CHECK2", "The list of checks passed to PMD run.sh -R, default: 'java-basic,java-sunsecure'" do |checks|
|
212
|
+
options[:pmd_checks] = checks
|
213
|
+
end
|
214
|
+
|
215
|
+
opts.separator ""
|
216
|
+
opts.separator "FindSecurityBugs options:"
|
217
|
+
|
218
|
+
opts.on "--findsecbugs-path PATH", "The full path to the base FindSecurityBugs directory" do |dir|
|
219
|
+
options[:findsecbugs_path] = dir
|
220
|
+
end
|
221
|
+
|
222
|
+
opts.separator ""
|
223
|
+
opts.separator "Configuration files:"
|
224
|
+
|
225
|
+
opts.on "-c", "--config-file FILE", "Use specified configuration file" do |file|
|
226
|
+
options[:config_file] = File.expand_path(file)
|
227
|
+
end
|
228
|
+
|
229
|
+
opts.on "-C", "--create-config [FILE]", "Output configuration file based on options" do |file|
|
230
|
+
if file
|
231
|
+
options[:create_config] = file
|
232
|
+
else
|
233
|
+
options[:create_config] = true
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
opts.separator ""
|
238
|
+
|
239
|
+
opts.on "-k", "--checks", "List all available vulnerability checks" do
|
240
|
+
options[:list_checks] = true
|
241
|
+
end
|
242
|
+
|
243
|
+
opts.on "--optional-checks", "List optional checks" do
|
244
|
+
options[:list_optional_checks] = true
|
245
|
+
end
|
246
|
+
|
247
|
+
opts.on "-v", "--version", "Show Glue version" do
|
248
|
+
options[:show_version] = true
|
249
|
+
end
|
250
|
+
|
251
|
+
opts.on_tail "-h", "--help", "Display this message" do
|
252
|
+
options[:show_help] = true
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
if destructive
|
257
|
+
parser.parse! args
|
258
|
+
else
|
259
|
+
parser.parse args
|
260
|
+
end
|
261
|
+
|
262
|
+
if options[:previous_results_json] and options[:output_files]
|
263
|
+
options[:comparison_output_file] = options[:output_files].shift
|
264
|
+
end
|
265
|
+
|
266
|
+
return options, parser
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class Glue::Reporters
|
2
|
+
@reporters = []
|
3
|
+
|
4
|
+
#Add a task. This will call +_klass_.new+ when running tests
|
5
|
+
def self.add klass
|
6
|
+
@reporters << klass unless @reporters.include? klass
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.reporters
|
10
|
+
@reporters
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.initialize_reporters reporters_directory = ""
|
14
|
+
#Load all files in task_directory
|
15
|
+
Dir.glob(File.join(reporters_directory, "*.rb")).sort.each do |f|
|
16
|
+
require f
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
#No need to use this directly.
|
21
|
+
def initialize options = { }
|
22
|
+
end
|
23
|
+
|
24
|
+
#Run all the tasks on the given Tracker.
|
25
|
+
#Returns a new instance of tasks with the results.
|
26
|
+
def self.run_report(tracker)
|
27
|
+
@reporters.each do |c|
|
28
|
+
reporter = c.new()
|
29
|
+
if tracker.options[:output_format].first == reporter.format
|
30
|
+
begin
|
31
|
+
output = reporter.run_report(tracker)
|
32
|
+
if tracker.options[:output_file]
|
33
|
+
file = File.open(tracker.options[:output_file], 'w'){ |f| f.write(output)}
|
34
|
+
else
|
35
|
+
Glue.notify output unless tracker.options[:quiet]
|
36
|
+
end
|
37
|
+
rescue => e
|
38
|
+
Glue.error e.message
|
39
|
+
tracker.error e
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
#Load all files in reporters/ directory
|
48
|
+
Dir.glob("#{File.expand_path(File.dirname(__FILE__))}/reporters/*.rb").sort.each do |f|
|
49
|
+
require f.match(/(glue\/reporters\/.*)\.rb$/)[0]
|
50
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'glue/finding'
|
2
|
+
|
3
|
+
class Glue::BaseReporter
|
4
|
+
attr_accessor :name, :format
|
5
|
+
|
6
|
+
def initialize()
|
7
|
+
end
|
8
|
+
|
9
|
+
def run_report(tracker)
|
10
|
+
Glue.notify "Running base reoprt..."
|
11
|
+
output = ""
|
12
|
+
tracker.findings.each do |finding|
|
13
|
+
output += out(finding)
|
14
|
+
end
|
15
|
+
output
|
16
|
+
end
|
17
|
+
|
18
|
+
def out(finding)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'glue/finding'
|
2
|
+
require 'glue/reporters/base_reporter'
|
3
|
+
|
4
|
+
class Glue::CSVReporter < Glue::BaseReporter
|
5
|
+
|
6
|
+
Glue::Reporters.add self
|
7
|
+
|
8
|
+
attr_accessor :name, :format
|
9
|
+
|
10
|
+
def initialize()
|
11
|
+
@name = "CSVReporter"
|
12
|
+
@format = :to_csv
|
13
|
+
end
|
14
|
+
|
15
|
+
def out(finding)
|
16
|
+
finding.to_csv
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'glue/finding'
|
2
|
+
require 'glue/reporters/base_reporter'
|
3
|
+
require 'json'
|
4
|
+
require 'curb'
|
5
|
+
|
6
|
+
class Glue::JiraReporter < Glue::BaseReporter
|
7
|
+
|
8
|
+
Glue::Reporters.add self
|
9
|
+
|
10
|
+
attr_accessor :name, :format
|
11
|
+
|
12
|
+
def initialize()
|
13
|
+
@name = "JiraReporter"
|
14
|
+
@format = :to_jira
|
15
|
+
end
|
16
|
+
|
17
|
+
def run_report(tracker)
|
18
|
+
@project = tracker.options[:jira_project.to_s]
|
19
|
+
@api = tracker.options[:jira_api_url.to_s]
|
20
|
+
@cookie = tracker.options[:jira_cookie.to_s]
|
21
|
+
@component = tracker.options[:jira_component.to_s]
|
22
|
+
|
23
|
+
tracker.findings.each do |finding|
|
24
|
+
report finding
|
25
|
+
end
|
26
|
+
"Results are in JIRA"
|
27
|
+
end
|
28
|
+
|
29
|
+
def report(finding)
|
30
|
+
json = get_jira_json(finding)
|
31
|
+
http = Curl.post("#{@api}/issue/", json.to_s) do |http|
|
32
|
+
http.headers['Content-Type'] = "application/json"
|
33
|
+
http.headers['Cookie'] = @cookie
|
34
|
+
end
|
35
|
+
if http.response_code != 201 # Created ...
|
36
|
+
Glue.error "Problem with HTTP #{http.response_code} - #{http.body_str}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def get_jira_json(finding)
|
42
|
+
json = {
|
43
|
+
"fields": {
|
44
|
+
"project":
|
45
|
+
{
|
46
|
+
"key": "#{@project}"
|
47
|
+
},
|
48
|
+
"summary": "#{finding.appname} - #{finding.description}",
|
49
|
+
"description": "#{finding.to_string}\n\nFINGERPRINT: #{finding.fingerprint}",
|
50
|
+
"issuetype": {
|
51
|
+
"name": "Task"
|
52
|
+
},
|
53
|
+
"labels":["Glue","#{finding.appname}"],
|
54
|
+
"components": [ { "name": "#{@component}" } ]
|
55
|
+
}
|
56
|
+
}.to_json
|
57
|
+
json
|
58
|
+
end
|
59
|
+
end
|