owasp-pipeline 0.8.3 → 0.8.5
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/lib/pipeline/mounters/filesystem_mounter.rb +2 -7
- data/lib/pipeline/options.rb +18 -0
- data/lib/pipeline/tasks/checkmarx.rb +4 -5
- data/lib/pipeline/tasks/dawnscanner.rb +56 -0
- data/lib/pipeline/tasks/findsecbugs.rb +81 -0
- data/lib/pipeline/tasks/pmd.rb +64 -0
- data/lib/pipeline/tasks/zap.rb +21 -8
- data/lib/pipeline/version.rb +1 -1
- metadata +75 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf5efee842fff5ee62a05d074fe47d057bc0dd1d
|
4
|
+
data.tar.gz: 417d5379d1b73b82a0a20561d5d78d50e907c020
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c44a4dc6c22acacac6b6d3a4b6197da7427ed3373341b28f2c4b1d01064b9d7d3357ae59c9a4020bf7fc2bb462d913db3f353038bbed328d556f427c7d68c12
|
7
|
+
data.tar.gz: 4a8f15ac36f2533638c0d8fc2d695168bc6bfa3bac7e3d41429b712d6e2b2618cd8b7c068f5b9d5140eaaaab9b9694e3e7100aad1a522e5fb75eefb3780a1206
|
@@ -2,7 +2,7 @@ require 'pipeline/mounters/base_mounter'
|
|
2
2
|
|
3
3
|
class Pipeline::FileSystemMounter < Pipeline::BaseMounter
|
4
4
|
Pipeline::Mounters.add self
|
5
|
-
|
5
|
+
|
6
6
|
def initialize trigger, options
|
7
7
|
super(trigger)
|
8
8
|
@options = options
|
@@ -15,11 +15,6 @@ class Pipeline::FileSystemMounter < Pipeline::BaseMounter
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def supports? target
|
18
|
-
|
19
|
-
if last === "/" or last === "."
|
20
|
-
return true
|
21
|
-
else
|
22
|
-
return false
|
23
|
-
end
|
18
|
+
File.directory? target
|
24
19
|
end
|
25
20
|
end
|
data/lib/pipeline/options.rb
CHANGED
@@ -190,6 +190,24 @@ module Pipeline::Options
|
|
190
190
|
options[:checkmarx_project] = project
|
191
191
|
end
|
192
192
|
|
193
|
+
opts.separator ""
|
194
|
+
opts.separator "PMD options:"
|
195
|
+
|
196
|
+
opts.on "--pmd-path PATH", "The full path to the base PMD directory" do |dir|
|
197
|
+
options[:pmd_path] = dir
|
198
|
+
end
|
199
|
+
|
200
|
+
opts.on "--pmd-checks CHECK1,CHECK2", "The list of checks passed to PMD run.sh -R, default: 'java-basic,java-sunsecure'" do |checks|
|
201
|
+
options[:pmd_checks] = checks
|
202
|
+
end
|
203
|
+
|
204
|
+
opts.separator ""
|
205
|
+
opts.separator "FindSecurityBugs options:"
|
206
|
+
|
207
|
+
opts.on "--findsecbugs-path PATH", "The full path to the base FindSecurityBugs directory" do |dir|
|
208
|
+
options[:findsecbugs_path] = dir
|
209
|
+
end
|
210
|
+
|
193
211
|
opts.separator ""
|
194
212
|
opts.separator "Configuration files:"
|
195
213
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'pipeline/tasks/base_task'
|
2
2
|
require 'pipeline/util'
|
3
|
-
|
3
|
+
require 'nokogiri'
|
4
4
|
|
5
5
|
class Pipeline::Checkmarx < Pipeline::BaseTask
|
6
6
|
|
@@ -28,7 +28,7 @@ class Pipeline::Checkmarx < Pipeline::BaseTask
|
|
28
28
|
"-ReportXML", "#{rootpath}checkmarx_results.xml",
|
29
29
|
"-Log", "#{@tracker.options[:checkmarx_log]}"
|
30
30
|
)
|
31
|
-
|
31
|
+
@results = Nokogiri::XML(File.read("#{rootpath}checkmarx_results.xml")).xpath '//Result'
|
32
32
|
end
|
33
33
|
|
34
34
|
def analyze
|
@@ -38,9 +38,9 @@ class Pipeline::Checkmarx < Pipeline::BaseTask
|
|
38
38
|
detail = result.attributes['DeepLink'].value
|
39
39
|
source = { :scanner => @name, :file => result.attributes['FileName'].value, :line => result.attributes['Line'].value.to_i, :code => result.at_xpath('Path/PathNode/Snippet/Line/Code').text }
|
40
40
|
sev = severity(result.parent.attributes['Severity'].value)
|
41
|
-
|
41
|
+
fprint = fingerprint("#{description}#{source}#{sev}")
|
42
42
|
|
43
|
-
report description, detail, source, sev,
|
43
|
+
report description, detail, source, sev, fprint
|
44
44
|
end
|
45
45
|
rescue Exception => e
|
46
46
|
Pipeline.warn e.message
|
@@ -59,4 +59,3 @@ class Pipeline::Checkmarx < Pipeline::BaseTask
|
|
59
59
|
end
|
60
60
|
|
61
61
|
end
|
62
|
-
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'pipeline/tasks/base_task'
|
2
|
+
require 'pipeline/util'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
class Pipeline::DawnScanner < Pipeline::BaseTask
|
6
|
+
|
7
|
+
Pipeline::Tasks.add self
|
8
|
+
include Pipeline::Util
|
9
|
+
|
10
|
+
def initialize(trigger, tracker)
|
11
|
+
super(trigger, tracker)
|
12
|
+
@name = "DawnScanner"
|
13
|
+
@description = "DawnScanner ruby analyzer"
|
14
|
+
@stage = :code
|
15
|
+
@labels << "code"
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
Pipeline.notify "#{@name}"
|
20
|
+
Dir.chdir("#{@trigger.path}") do
|
21
|
+
@results_file = Tempfile.new(['dawnresults', 'xml'])
|
22
|
+
runsystem(true, "dawn", "-F", "#{@results_file.path}", "-j", ".")
|
23
|
+
@results = JSON.parse(File.read("#{@results_file.path}"))['vulnerabilities']
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def analyze
|
28
|
+
begin
|
29
|
+
@results.each do |result|
|
30
|
+
description = result['name'].gsub('\n',' ')
|
31
|
+
detail = "#{result['message']}\n#{result['remediation']}\n#{result['cve_link']}"
|
32
|
+
source = {:scanner => @name, :file => nil, :line => nil, :code => nil}
|
33
|
+
sev = severity(result['severity'])
|
34
|
+
fprint = fingerprint("#{description}#{detail}#{source}#{sev}")
|
35
|
+
|
36
|
+
report description, detail, source, sev, fprint
|
37
|
+
end
|
38
|
+
rescue Exception => e
|
39
|
+
Pipeline.warn e.message
|
40
|
+
Pipeline.warn e.backtrace
|
41
|
+
ensure
|
42
|
+
File.unlink @results_file
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def supported?
|
47
|
+
supported=runsystem(true, "dawn", "--version")
|
48
|
+
if supported =~ /command not found/
|
49
|
+
Pipeline.notify "Install dawnscanner: 'gem install dawnscanner'"
|
50
|
+
return false
|
51
|
+
else
|
52
|
+
return true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'pipeline/tasks/base_task'
|
2
|
+
require 'pipeline/util'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'mkmf'
|
6
|
+
|
7
|
+
MakeMakefile::Logging.instance_variable_set(:@logfile, File::NULL)
|
8
|
+
|
9
|
+
class Pipeline::FindSecurityBugs < Pipeline::BaseTask
|
10
|
+
|
11
|
+
Pipeline::Tasks.add self
|
12
|
+
include Pipeline::Util
|
13
|
+
|
14
|
+
def initialize(trigger, tracker)
|
15
|
+
super(trigger, tracker)
|
16
|
+
@name = "FindSecurityBugs"
|
17
|
+
@description = "FindSecurityBugs plugin for FindBugs"
|
18
|
+
@stage = :code
|
19
|
+
@labels << "code"
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
Pipeline.notify "#{@name}"
|
24
|
+
@results_file = Tempfile.new(['findsecbugs','xml'])
|
25
|
+
|
26
|
+
Dir.chdir("#{@trigger.path}") do
|
27
|
+
runsystem(true, "mvn", "compile", "-fn")
|
28
|
+
end
|
29
|
+
|
30
|
+
Dir.chdir("#{@tracker.options[:findsecbugs_path]}") do
|
31
|
+
runsystem(true, "/bin/sh", "#{@tracker.options[:findsecbugs_path]}/findsecbugs.sh", "-effort:max", "-quiet", "-xml:withMessages", "-output", "#{@results_file.path}", "#{@trigger.path}")
|
32
|
+
@results = Nokogiri::XML(File.read(@results_file)).xpath '//BugInstance'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def analyze
|
37
|
+
begin
|
38
|
+
@results.each do |result|
|
39
|
+
description = result.xpath('ShortMessage').text
|
40
|
+
bug_type = result.attributes['type'].value
|
41
|
+
detail = "Class: #{result.at_xpath('Method').attributes['classname'].value}, Method: #{result.at_xpath('Method').attributes['name'].value}\n#{result.xpath('LongMessage').text}\nhttps://find-sec-bugs.github.io/bugs.htm##{bug_type}"
|
42
|
+
|
43
|
+
file = result.at_xpath('SourceLine').attributes['sourcepath'].value
|
44
|
+
trigger_path = Pathname.new(@trigger.path)
|
45
|
+
real_path = nil
|
46
|
+
trigger_path.find {|path| real_path = path if path.fnmatch "*/#{file}"}
|
47
|
+
file = real_path.relative_path_from(trigger_path).to_s unless real_path.nil?
|
48
|
+
|
49
|
+
line = result.at_xpath('SourceLine[@primary="true"]').attributes['start'].value
|
50
|
+
code = "#{result.at_xpath('String').attributes['value'].value}"
|
51
|
+
source = {:scanner => @name, :file => file, :line => line, :code => code}
|
52
|
+
sev = result.attributes['priority'].value
|
53
|
+
fprint = fingerprint("#{description}#{detail}#{source}")
|
54
|
+
|
55
|
+
report description, detail, source, sev, fprint
|
56
|
+
end
|
57
|
+
rescue Exception => e
|
58
|
+
Pipeline.warn e.message
|
59
|
+
Pipeline.warn e.backtrace
|
60
|
+
ensure
|
61
|
+
File.unlink @results_file
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def supported?
|
66
|
+
unless find_executable0('mvn') and File.exist?("#{@trigger.path}/pom.xml")
|
67
|
+
Pipeline.notify "FindSecurityBugs support requires maven and pom.xml"
|
68
|
+
Pipeline.notify "Please install maven somewhere in your PATH and include a valid pom.xml in the project root"
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
|
72
|
+
unless @tracker.options.has_key?(:findsecbugs_path) and File.exist?("#{@tracker.options[:findsecbugs_path]}/findsecbugs.sh")
|
73
|
+
Pipeline.notify "#{@tracker.options[:findsecbugs_path]}"
|
74
|
+
Pipeline.notify "Download and unpack the latest findsecbugs-cli release: https://github.com/find-sec-bugs/find-sec-bugs/releases"
|
75
|
+
return false
|
76
|
+
else
|
77
|
+
return true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'pipeline/tasks/base_task'
|
2
|
+
require 'pipeline/util'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
class Pipeline::PMD < Pipeline::BaseTask
|
7
|
+
|
8
|
+
Pipeline::Tasks.add self
|
9
|
+
include Pipeline::Util
|
10
|
+
|
11
|
+
def initialize(trigger, tracker)
|
12
|
+
super(trigger, tracker)
|
13
|
+
@name = "PMD"
|
14
|
+
@description = "PMD Source Code Analyzer"
|
15
|
+
@stage = :code
|
16
|
+
@labels << "code"
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
Pipeline.notify "#{@name}"
|
21
|
+
@tracker.options[:pmd_checks] ||= "java-basic,java-sunsecure"
|
22
|
+
Dir.chdir @tracker.options[:pmd_path] do
|
23
|
+
@results = Nokogiri::XML(`bin/run.sh pmd -d #{@trigger.path} -f xml -R #{@tracker.options[:pmd_checks]}`).xpath('//file')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def analyze
|
28
|
+
begin
|
29
|
+
@results.each do |result|
|
30
|
+
attributes = result.at_xpath('violation').attributes
|
31
|
+
description = result.children.children.to_s.strip
|
32
|
+
detail = "Ruleset: #{attributes['ruleset']}"
|
33
|
+
source = {:scanner => @name, :file => result.attributes['name'].to_s.split(Pathname.new(@trigger.path).cleanpath.to_s)[1][1..-1], :line => attributes['beginline'].to_s, :code => "package: #{attributes['package'].to_s}\nclass: #{attributes['class'].to_s}\nmethod: #{attributes['method'].to_s}" }
|
34
|
+
case attributes['priority'].value.to_i
|
35
|
+
when 3
|
36
|
+
sev = 1
|
37
|
+
when 2
|
38
|
+
sev = 2
|
39
|
+
when 1
|
40
|
+
sev = 3
|
41
|
+
else
|
42
|
+
sev = 0
|
43
|
+
end
|
44
|
+
fprint = fingerprint("#{description}#{detail}#{source}#{sev}")
|
45
|
+
|
46
|
+
report description, detail, source, sev, fprint
|
47
|
+
end
|
48
|
+
rescue Exception => e
|
49
|
+
Pipeline.warn e.message
|
50
|
+
Pipeline.warn e.backtrace
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def supported?
|
55
|
+
unless @tracker.options.has_key?(:pmd_path) and File.exist?("#{@tracker.options[:pmd_path]}/bin/run.sh")
|
56
|
+
Pipeline.notify "#{@tracker.options[:pmd_path]}"
|
57
|
+
Pipeline.notify "Install PMD from: https://pmd.github.io/"
|
58
|
+
return false
|
59
|
+
else
|
60
|
+
return true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
data/lib/pipeline/tasks/zap.rb
CHANGED
@@ -2,6 +2,7 @@ require 'pipeline/tasks/base_task'
|
|
2
2
|
require 'pipeline/util'
|
3
3
|
require 'json'
|
4
4
|
require 'curb'
|
5
|
+
require 'securerandom'
|
5
6
|
|
6
7
|
class Pipeline::Zap < Pipeline::BaseTask
|
7
8
|
|
@@ -19,21 +20,33 @@ class Pipeline::Zap < Pipeline::BaseTask
|
|
19
20
|
def run
|
20
21
|
rootpath = @trigger.path
|
21
22
|
base = "#{@tracker.options[:zap_host]}:#{@tracker.options[:zap_port]}"
|
22
|
-
|
23
|
+
apikey = "#{@tracker.options[:zap_api_token]}"
|
24
|
+
context = SecureRandom.uuid
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
+
Pipeline.debug "Running ZAP on: #{rootpath} from #{base} with #{context}"
|
27
|
+
|
28
|
+
# Set up Context
|
29
|
+
Curl.get("#{base}/JSON/context/action/newContext/?&apikey=#{apikey}&contextName=#{context}")
|
30
|
+
Curl.get("#{base}/JSON/context/action/includeInContext/?apikey=#{apikey}&contextName=#{context}®ex=#{rootpath}.*")
|
26
31
|
|
27
32
|
# Spider
|
28
|
-
Curl.get("#{base}/JSON/spider/action/scan
|
29
|
-
poll_until_100("#{base}/JSON/spider/view/status")
|
33
|
+
spider = get_scan_id( Curl.get("#{base}/JSON/spider/action/scan/?apikey=#{apikey}&url=#{rootpath}&context=#{context}") )
|
34
|
+
poll_until_100("#{base}/JSON/spider/view/status/?scanId=#{spider}")
|
30
35
|
|
31
36
|
# Active Scan
|
32
|
-
Curl.get("#{base}/JSON/ascan/action/scan/?recurse=true&inScopeOnly=true&url=#{rootpath}")
|
33
|
-
poll_until_100("#{base}/JSON/ascan/view/status
|
37
|
+
scan = get_scan_id ( Curl.get("#{base}/JSON/ascan/action/scan/?apikey=#{apikey}&recurse=true&inScopeOnly=true&url=#{rootpath}") )
|
38
|
+
poll_until_100("#{base}/JSON/ascan/view/status/?scanId=#{scan}")
|
34
39
|
|
35
40
|
# Result
|
36
|
-
@result = Curl.get("#{base}/JSON/core/view/alerts
|
41
|
+
@result = Curl.get("#{base}/JSON/core/view/alerts/?baseurl=#{rootpath}").body_str
|
42
|
+
|
43
|
+
# Remove Context
|
44
|
+
Curl.get("#{base}/JSON/context/action/removeContext/?&apikey=#{apikey}&contextName=#{context}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_scan_id(response)
|
48
|
+
json = JSON.parse response.body_str
|
49
|
+
return json["scan"]
|
37
50
|
end
|
38
51
|
|
39
52
|
def poll_until_100(url)
|
data/lib/pipeline/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: owasp-pipeline
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Konda
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2016-
|
13
|
+
date: 2016-03-07 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: terminal-table
|
@@ -124,6 +124,76 @@ dependencies:
|
|
124
124
|
- - ">="
|
125
125
|
- !ruby/object:Gem::Version
|
126
126
|
version: 0.5.7
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: nokogiri
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: 1.6.6.2
|
134
|
+
type: :runtime
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: 1.6.6.2
|
141
|
+
- !ruby/object:Gem::Dependency
|
142
|
+
name: rake
|
143
|
+
requirement: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
type: :runtime
|
149
|
+
prerelease: false
|
150
|
+
version_requirements: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ">="
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
- !ruby/object:Gem::Dependency
|
156
|
+
name: dawnscanner
|
157
|
+
requirement: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: 1.6.0
|
162
|
+
type: :runtime
|
163
|
+
prerelease: false
|
164
|
+
version_requirements: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: 1.6.0
|
169
|
+
- !ruby/object:Gem::Dependency
|
170
|
+
name: pry
|
171
|
+
requirement: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
type: :development
|
177
|
+
prerelease: false
|
178
|
+
version_requirements: !ruby/object:Gem::Requirement
|
179
|
+
requirements:
|
180
|
+
- - ">="
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: '0'
|
183
|
+
- !ruby/object:Gem::Dependency
|
184
|
+
name: pry-byebug
|
185
|
+
requirement: !ruby/object:Gem::Requirement
|
186
|
+
requirements:
|
187
|
+
- - ">="
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
190
|
+
type: :development
|
191
|
+
prerelease: false
|
192
|
+
version_requirements: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - ">="
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '0'
|
127
197
|
description: Pipeline detects security vulnerabilities in code.
|
128
198
|
email: matt.konda@owasp.org
|
129
199
|
executables:
|
@@ -163,11 +233,14 @@ files:
|
|
163
233
|
- lib/pipeline/tasks/brakeman.rb
|
164
234
|
- lib/pipeline/tasks/bundle-audit.rb
|
165
235
|
- lib/pipeline/tasks/checkmarx.rb
|
236
|
+
- lib/pipeline/tasks/dawnscanner.rb
|
166
237
|
- lib/pipeline/tasks/eslint.rb
|
167
238
|
- lib/pipeline/tasks/fim.rb
|
239
|
+
- lib/pipeline/tasks/findsecbugs.rb
|
168
240
|
- lib/pipeline/tasks/nsp.rb
|
169
241
|
- lib/pipeline/tasks/owasp-dep-check.rb
|
170
242
|
- lib/pipeline/tasks/patterns.json
|
243
|
+
- lib/pipeline/tasks/pmd.rb
|
171
244
|
- lib/pipeline/tasks/retirejs.rb
|
172
245
|
- lib/pipeline/tasks/scanjs-eslintrc
|
173
246
|
- lib/pipeline/tasks/scanjs.rb
|