codeclimate-fede 0.85.21
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/bin/check +18 -0
- data/bin/codeclimate +21 -0
- data/bin/prep-release +45 -0
- data/bin/publish +47 -0
- data/bin/release +41 -0
- data/bin/validate-release +18 -0
- data/config/engines.yml +322 -0
- data/lib/cc/analyzer/bridge.rb +106 -0
- data/lib/cc/analyzer/composite_container_listener.rb +21 -0
- data/lib/cc/analyzer/container/result.rb +74 -0
- data/lib/cc/analyzer/container.rb +208 -0
- data/lib/cc/analyzer/container_listener.rb +9 -0
- data/lib/cc/analyzer/engine.rb +125 -0
- data/lib/cc/analyzer/engine_output.rb +74 -0
- data/lib/cc/analyzer/engine_output_filter.rb +36 -0
- data/lib/cc/analyzer/engine_output_overrider.rb +31 -0
- data/lib/cc/analyzer/filesystem.rb +50 -0
- data/lib/cc/analyzer/formatters/formatter.rb +53 -0
- data/lib/cc/analyzer/formatters/html_formatter.rb +415 -0
- data/lib/cc/analyzer/formatters/json_formatter.rb +38 -0
- data/lib/cc/analyzer/formatters/plain_text_formatter.rb +101 -0
- data/lib/cc/analyzer/formatters/spinner.rb +35 -0
- data/lib/cc/analyzer/formatters.rb +21 -0
- data/lib/cc/analyzer/issue.rb +69 -0
- data/lib/cc/analyzer/issue_sorter.rb +30 -0
- data/lib/cc/analyzer/issue_validations/category_validation.rb +32 -0
- data/lib/cc/analyzer/issue_validations/check_name_presence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/content_validation.rb +21 -0
- data/lib/cc/analyzer/issue_validations/description_presence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/location_format_validation.rb +72 -0
- data/lib/cc/analyzer/issue_validations/other_locations_format_validation.rb +41 -0
- data/lib/cc/analyzer/issue_validations/path_existence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/path_is_file_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/path_presence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/relative_path_validation.rb +32 -0
- data/lib/cc/analyzer/issue_validations/remediation_points_validation.rb +25 -0
- data/lib/cc/analyzer/issue_validations/severity_validation.rb +39 -0
- data/lib/cc/analyzer/issue_validations/type_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/validation.rb +35 -0
- data/lib/cc/analyzer/issue_validations.rb +26 -0
- data/lib/cc/analyzer/issue_validator.rb +11 -0
- data/lib/cc/analyzer/location_description.rb +45 -0
- data/lib/cc/analyzer/logging_container_listener.rb +24 -0
- data/lib/cc/analyzer/measurement.rb +22 -0
- data/lib/cc/analyzer/measurement_validations/name_validation.rb +23 -0
- data/lib/cc/analyzer/measurement_validations/type_validation.rb +15 -0
- data/lib/cc/analyzer/measurement_validations/validation.rb +27 -0
- data/lib/cc/analyzer/measurement_validations/value_validation.rb +21 -0
- data/lib/cc/analyzer/measurement_validations.rb +16 -0
- data/lib/cc/analyzer/measurement_validator.rb +11 -0
- data/lib/cc/analyzer/mounted_path.rb +80 -0
- data/lib/cc/analyzer/raising_container_listener.rb +32 -0
- data/lib/cc/analyzer/source_buffer.rb +47 -0
- data/lib/cc/analyzer/source_extractor.rb +79 -0
- data/lib/cc/analyzer/source_fingerprint.rb +40 -0
- data/lib/cc/analyzer/statsd_container_listener.rb +51 -0
- data/lib/cc/analyzer/validator.rb +38 -0
- data/lib/cc/analyzer.rb +50 -0
- data/lib/cc/cli/analyze/engine_failure.rb +11 -0
- data/lib/cc/cli/analyze.rb +90 -0
- data/lib/cc/cli/command.rb +85 -0
- data/lib/cc/cli/console.rb +12 -0
- data/lib/cc/cli/engines/engine_command.rb +15 -0
- data/lib/cc/cli/engines/install.rb +35 -0
- data/lib/cc/cli/engines/list.rb +18 -0
- data/lib/cc/cli/engines.rb +5 -0
- data/lib/cc/cli/file_store.rb +42 -0
- data/lib/cc/cli/global_cache.rb +47 -0
- data/lib/cc/cli/global_config.rb +35 -0
- data/lib/cc/cli/help.rb +51 -0
- data/lib/cc/cli/output.rb +34 -0
- data/lib/cc/cli/prepare.rb +98 -0
- data/lib/cc/cli/runner.rb +75 -0
- data/lib/cc/cli/validate_config.rb +84 -0
- data/lib/cc/cli/version.rb +16 -0
- data/lib/cc/cli/version_checker.rb +107 -0
- data/lib/cc/cli.rb +39 -0
- data/lib/cc/config/checks_adapter.rb +40 -0
- data/lib/cc/config/default_adapter.rb +54 -0
- data/lib/cc/config/engine.rb +41 -0
- data/lib/cc/config/engine_set.rb +47 -0
- data/lib/cc/config/json_adapter.rb +17 -0
- data/lib/cc/config/prepare.rb +92 -0
- data/lib/cc/config/validation/check_validator.rb +34 -0
- data/lib/cc/config/validation/engine_validator.rb +93 -0
- data/lib/cc/config/validation/fetch_validator.rb +78 -0
- data/lib/cc/config/validation/file_validator.rb +112 -0
- data/lib/cc/config/validation/hash_validations.rb +52 -0
- data/lib/cc/config/validation/json.rb +31 -0
- data/lib/cc/config/validation/prepare_validator.rb +40 -0
- data/lib/cc/config/validation/yaml.rb +66 -0
- data/lib/cc/config/yaml_adapter.rb +73 -0
- data/lib/cc/config.rb +70 -0
- data/lib/cc/engine_registry.rb +74 -0
- data/lib/cc/resolv.rb +39 -0
- data/lib/cc/workspace/exclusion.rb +34 -0
- data/lib/cc/workspace/path_tree/dir_node.rb +67 -0
- data/lib/cc/workspace/path_tree/file_node.rb +31 -0
- data/lib/cc/workspace/path_tree.rb +49 -0
- data/lib/cc/workspace.rb +39 -0
- metadata +279 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module CC
|
|
2
|
+
module Analyzer
|
|
3
|
+
class Container
|
|
4
|
+
class Result
|
|
5
|
+
attr_reader \
|
|
6
|
+
:container_name,
|
|
7
|
+
:duration,
|
|
8
|
+
:exit_status,
|
|
9
|
+
:output_byte_count,
|
|
10
|
+
:stderr,
|
|
11
|
+
:stdout
|
|
12
|
+
|
|
13
|
+
def initialize(
|
|
14
|
+
container_name: "",
|
|
15
|
+
duration: 0,
|
|
16
|
+
exit_status: 0,
|
|
17
|
+
maximum_output_exceeded: false,
|
|
18
|
+
output_byte_count: 0,
|
|
19
|
+
skipped: false,
|
|
20
|
+
stderr: "",
|
|
21
|
+
stdout: "",
|
|
22
|
+
timed_out: false
|
|
23
|
+
)
|
|
24
|
+
@container_name = container_name
|
|
25
|
+
@duration = duration
|
|
26
|
+
@exit_status = exit_status
|
|
27
|
+
@maximum_output_exceeded = maximum_output_exceeded
|
|
28
|
+
@output_byte_count = output_byte_count
|
|
29
|
+
@skipped = skipped
|
|
30
|
+
@stderr = stderr
|
|
31
|
+
@stdout = stdout
|
|
32
|
+
@timed_out = timed_out
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.skipped(ex)
|
|
36
|
+
new(
|
|
37
|
+
exit_status: 0,
|
|
38
|
+
skipped: true,
|
|
39
|
+
stderr: ex.message,
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def merge_from_exception(ex)
|
|
44
|
+
self.exit_status = 99
|
|
45
|
+
self.stderr = ex.message
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def timed_out?
|
|
50
|
+
@timed_out
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def maximum_output_exceeded?
|
|
54
|
+
@maximum_output_exceeded
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def errored?
|
|
58
|
+
timed_out? ||
|
|
59
|
+
maximum_output_exceeded? ||
|
|
60
|
+
exit_status.nil? ||
|
|
61
|
+
exit_status.nonzero?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def skipped?
|
|
65
|
+
@skipped
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
attr_writer :exit_status, :stderr
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
require "posix/spawn"
|
|
2
|
+
require "thread"
|
|
3
|
+
|
|
4
|
+
require "cc/analyzer/container/result"
|
|
5
|
+
|
|
6
|
+
module CC
|
|
7
|
+
module Analyzer
|
|
8
|
+
#
|
|
9
|
+
# Running an abstract docker container
|
|
10
|
+
#
|
|
11
|
+
# Input:
|
|
12
|
+
# - image
|
|
13
|
+
# - name
|
|
14
|
+
# - command (Optional)
|
|
15
|
+
#
|
|
16
|
+
# Output:
|
|
17
|
+
# - Result
|
|
18
|
+
# - exit_status
|
|
19
|
+
# - timed_out?
|
|
20
|
+
# - duration
|
|
21
|
+
# - maximum_output_exceeded?
|
|
22
|
+
# - output_byte_count
|
|
23
|
+
# - stderr
|
|
24
|
+
#
|
|
25
|
+
# Never raises (unless broken)
|
|
26
|
+
#
|
|
27
|
+
class Container
|
|
28
|
+
DEFAULT_TIMEOUT = 15 * 60 # 15m
|
|
29
|
+
DEFAULT_MAXIMUM_OUTPUT_BYTES = 500_000_000
|
|
30
|
+
|
|
31
|
+
def initialize(image:, name:, command: nil)
|
|
32
|
+
@image = image
|
|
33
|
+
@name = name
|
|
34
|
+
@command = command
|
|
35
|
+
@timed_out = false
|
|
36
|
+
@maximum_output_exceeded = false
|
|
37
|
+
@stdout_io = StringIO.new
|
|
38
|
+
@stderr_io = StringIO.new
|
|
39
|
+
@output_byte_count = 0
|
|
40
|
+
@counter_mutex = Mutex.new
|
|
41
|
+
|
|
42
|
+
# By default accumulate and include stdout in result
|
|
43
|
+
@output_delimeter = "\n"
|
|
44
|
+
@on_output = ->(output) { @stdout_io.puts(output) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def on_output(delimeter = "\n", &block)
|
|
48
|
+
@output_delimeter = delimeter
|
|
49
|
+
@on_output = block
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def run(options = [])
|
|
53
|
+
started = Time.now
|
|
54
|
+
|
|
55
|
+
command = docker_run_command(options)
|
|
56
|
+
Analyzer.logger.debug("docker run: #{command.inspect}")
|
|
57
|
+
pid, _, out, err = POSIX::Spawn.popen4(*command)
|
|
58
|
+
|
|
59
|
+
@t_out = read_stdout(out)
|
|
60
|
+
@t_err = read_stderr(err)
|
|
61
|
+
t_timeout = timeout_thread
|
|
62
|
+
|
|
63
|
+
# blocks until the engine stops. this is put in a thread so that we can
|
|
64
|
+
# explicitly abort it as part of #stop. otherwise a run-away container
|
|
65
|
+
# could still block here forever if the docker-kill/wait is not
|
|
66
|
+
# successful. there may still be stdout in flight if it was being
|
|
67
|
+
# produced more quickly than consumed.
|
|
68
|
+
@t_wait = Thread.new { _, @status = Process.waitpid2(pid) }
|
|
69
|
+
@t_wait.join
|
|
70
|
+
|
|
71
|
+
# blocks until all readers are done. they're still governed by the
|
|
72
|
+
# timeout thread at this point. if we hit the timeout while processing
|
|
73
|
+
# output, the threads will be Thread#killed as part of #stop and this
|
|
74
|
+
# will unblock with the correct value in @timed_out
|
|
75
|
+
[@t_out, @t_err].each(&:join)
|
|
76
|
+
|
|
77
|
+
duration =
|
|
78
|
+
if @timed_out
|
|
79
|
+
timeout * 1000
|
|
80
|
+
else
|
|
81
|
+
((Time.now - started) * 1000).round
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
Result.new(
|
|
85
|
+
container_name: @name,
|
|
86
|
+
duration: duration,
|
|
87
|
+
exit_status: @status&.exitstatus,
|
|
88
|
+
maximum_output_exceeded: @maximum_output_exceeded,
|
|
89
|
+
output_byte_count: output_byte_count,
|
|
90
|
+
stderr: @stderr_io.string,
|
|
91
|
+
stdout: @stdout_io.string,
|
|
92
|
+
timed_out: @timed_out,
|
|
93
|
+
)
|
|
94
|
+
ensure
|
|
95
|
+
kill_reader_threads
|
|
96
|
+
t_timeout&.kill
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def stop(message = nil)
|
|
100
|
+
reap_running_container(message)
|
|
101
|
+
kill_reader_threads
|
|
102
|
+
kill_wait_thread
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
attr_reader :output_byte_count, :counter_mutex
|
|
108
|
+
|
|
109
|
+
def docker_run_command(options)
|
|
110
|
+
[
|
|
111
|
+
"docker", "run",
|
|
112
|
+
"--name", @name,
|
|
113
|
+
options,
|
|
114
|
+
@image,
|
|
115
|
+
@command
|
|
116
|
+
].flatten.compact
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def read_stdout(out)
|
|
120
|
+
Thread.new do
|
|
121
|
+
out.each_line(@output_delimeter) do |chunk|
|
|
122
|
+
output = chunk.chomp(@output_delimeter)
|
|
123
|
+
|
|
124
|
+
Analyzer.logger.debug("engine stdout: #{output}")
|
|
125
|
+
@on_output.call(output)
|
|
126
|
+
check_output_bytes(output.bytesize)
|
|
127
|
+
end
|
|
128
|
+
ensure
|
|
129
|
+
out.close
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def read_stderr(err)
|
|
134
|
+
Thread.new do
|
|
135
|
+
err.each_line do |line|
|
|
136
|
+
Analyzer.logger.debug("engine stderr: #{line.chomp}")
|
|
137
|
+
@stderr_io.write(line)
|
|
138
|
+
check_output_bytes(line.bytesize)
|
|
139
|
+
end
|
|
140
|
+
ensure
|
|
141
|
+
err.close
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def timeout_thread
|
|
146
|
+
Thread.new do
|
|
147
|
+
# Doing one long `sleep timeout` seems to fail sometimes, so
|
|
148
|
+
# we do a series of short timeouts before exiting
|
|
149
|
+
start_time = Time.now
|
|
150
|
+
loop do
|
|
151
|
+
sleep 10
|
|
152
|
+
duration = Time.now - start_time
|
|
153
|
+
break if duration >= timeout
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
@timed_out = true
|
|
157
|
+
stop("timed out")
|
|
158
|
+
end.run
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def check_output_bytes(last_read_byte_count)
|
|
162
|
+
counter_mutex.synchronize do
|
|
163
|
+
@output_byte_count += last_read_byte_count
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if output_byte_count > maximum_output_bytes
|
|
167
|
+
@maximum_output_exceeded = true
|
|
168
|
+
stop("maximum output exceeded")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def kill_reader_threads
|
|
173
|
+
@t_out&.kill
|
|
174
|
+
@t_err&.kill
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def kill_wait_thread
|
|
178
|
+
@t_wait&.kill
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def reap_running_container(message)
|
|
182
|
+
Analyzer.logger.warn("killing container name=#{@name} message=#{message.inspect}")
|
|
183
|
+
POSIX::Spawn::Child.new("docker", "kill", @name, timeout: 2.minutes)
|
|
184
|
+
POSIX::Spawn::Child.new("docker", "wait", @name, timeout: 2.minutes)
|
|
185
|
+
rescue POSIX::Spawn::TimeoutExceeded
|
|
186
|
+
Analyzer.logger.error("unable to kill container name=#{@name} message=#{message.inspect}")
|
|
187
|
+
Analyzer.statsd.increment("container.zombie")
|
|
188
|
+
Analyzer.statsd.increment("container.zombie.#{metric_name}") if metric_name
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def timeout
|
|
192
|
+
ENV.fetch("CONTAINER_TIMEOUT_SECONDS", DEFAULT_TIMEOUT).to_i
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def maximum_output_bytes
|
|
196
|
+
ENV.fetch("CONTAINER_MAXIMUM_OUTPUT_BYTES", DEFAULT_MAXIMUM_OUTPUT_BYTES).to_i
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def metric_name
|
|
200
|
+
if /^cc-engines-(?<engine>[^-]+)-(?<channel>[^-]+)-/ =~ @name
|
|
201
|
+
"engine.#{engine}.#{channel}"
|
|
202
|
+
elsif /^builder-(?<action>[^-]+)-/ =~ @name
|
|
203
|
+
"builder.#{action}"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module CC
|
|
4
|
+
module Analyzer
|
|
5
|
+
#
|
|
6
|
+
# Running specifically an Engine container
|
|
7
|
+
#
|
|
8
|
+
# Input:
|
|
9
|
+
# - name
|
|
10
|
+
# - metadata
|
|
11
|
+
# - image
|
|
12
|
+
# - command (optional)
|
|
13
|
+
# - config (becomes /config.json)
|
|
14
|
+
# - label
|
|
15
|
+
# - io (to write filtered, validated output)
|
|
16
|
+
#
|
|
17
|
+
# Output:
|
|
18
|
+
# - Container::Result
|
|
19
|
+
#
|
|
20
|
+
class Engine
|
|
21
|
+
Error = Class.new(StandardError)
|
|
22
|
+
|
|
23
|
+
def initialize(name, metadata, config, label)
|
|
24
|
+
@name = name
|
|
25
|
+
@metadata = metadata
|
|
26
|
+
@config = config
|
|
27
|
+
@label = label.to_s
|
|
28
|
+
@error = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run(io)
|
|
32
|
+
write_config_file
|
|
33
|
+
|
|
34
|
+
container = Container.new(
|
|
35
|
+
image: metadata.fetch("image"),
|
|
36
|
+
command: metadata["command"],
|
|
37
|
+
name: container_name,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
container.on_output("\0") do |output|
|
|
41
|
+
handle_output(container, io, output)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
container.run(container_options).tap do |result|
|
|
45
|
+
result.merge_from_exception(error) if error.present?
|
|
46
|
+
end
|
|
47
|
+
ensure
|
|
48
|
+
delete_config_file
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
attr_reader :name, :metadata
|
|
54
|
+
attr_accessor :error
|
|
55
|
+
|
|
56
|
+
def handle_output(container, io, raw_output)
|
|
57
|
+
output = EngineOutput.new(name, raw_output)
|
|
58
|
+
|
|
59
|
+
return if output_filter.filter?(output)
|
|
60
|
+
|
|
61
|
+
unless output.valid?
|
|
62
|
+
self.error = Error.new("engine produced invalid output: #{output.error}")
|
|
63
|
+
container.stop("output invalid")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
unless io.write(output_overrider.apply(output).to_json)
|
|
67
|
+
self.error = Error.new("#{io.class}#write returned false, indicating an error")
|
|
68
|
+
container.stop("output error")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def qualified_name
|
|
73
|
+
"#{name}:#{@config.fetch("channel", "stable")}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def container_options
|
|
77
|
+
options = [
|
|
78
|
+
"--cap-drop", "all",
|
|
79
|
+
"--label", "com.codeclimate.label=#{@label}",
|
|
80
|
+
"--log-driver", "none",
|
|
81
|
+
"--memory-swap", "-1",
|
|
82
|
+
"--net", "none",
|
|
83
|
+
"--rm",
|
|
84
|
+
"--volume", "#{code.host_path}:/code:ro",
|
|
85
|
+
"--volume", "#{config_file.host_path}:/config.json:ro",
|
|
86
|
+
"--user", "9000:9000"
|
|
87
|
+
]
|
|
88
|
+
if (memory = metadata["memory"]).present?
|
|
89
|
+
options.concat(["--memory", memory.to_s])
|
|
90
|
+
end
|
|
91
|
+
options
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def container_name
|
|
95
|
+
@container_name ||= "cc-engines-#{qualified_name.tr(":", "-")}-#{SecureRandom.uuid}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def write_config_file
|
|
99
|
+
@config["debug"] = ENV["CODECLIMATE_DEBUG"]
|
|
100
|
+
Analyzer.logger.debug "/config.json content: #{@config.inspect}"
|
|
101
|
+
config_file.write(@config.to_json)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def delete_config_file
|
|
105
|
+
config_file.delete if config_file.file?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def code
|
|
109
|
+
@code ||= MountedPath.code
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def config_file
|
|
113
|
+
@config_file ||= MountedPath.tmp.join(SecureRandom.uuid)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def output_filter
|
|
117
|
+
@output_filter ||= EngineOutputFilter.new(@config)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def output_overrider
|
|
121
|
+
@output_overrider ||= EngineOutputOverrider.new(@config)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module CC
|
|
2
|
+
module Analyzer
|
|
3
|
+
class EngineOutput
|
|
4
|
+
delegate :blank?, to: :raw_output
|
|
5
|
+
|
|
6
|
+
def initialize(name, raw_output)
|
|
7
|
+
@name = name
|
|
8
|
+
@raw_output = raw_output
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def issue?
|
|
12
|
+
valid_with_type?("issue")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def measurement?
|
|
16
|
+
valid_with_type?("measurement")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def as_issue
|
|
20
|
+
Issue.new(name, raw_output)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_json
|
|
24
|
+
if issue?
|
|
25
|
+
as_issue.to_json
|
|
26
|
+
elsif measurement?
|
|
27
|
+
Measurement.new(name, raw_output).to_json
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def valid?
|
|
32
|
+
valid_json? && validator && validator.valid?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def error
|
|
36
|
+
if !valid_json?
|
|
37
|
+
{ message: "Invalid JSON", output: raw_output }
|
|
38
|
+
elsif !validator.present?
|
|
39
|
+
{ message: "Unsupported document type", output: raw_output }
|
|
40
|
+
else
|
|
41
|
+
validator.error
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
attr_accessor :name, :raw_output
|
|
48
|
+
|
|
49
|
+
def valid_json?
|
|
50
|
+
parsed_output.present?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def valid_with_type?(type)
|
|
54
|
+
parsed_output &&
|
|
55
|
+
parsed_output["type"].present? &&
|
|
56
|
+
parsed_output["type"].downcase == type
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parsed_output
|
|
60
|
+
@parsed_output ||= JSON.parse(raw_output)
|
|
61
|
+
rescue JSON::ParserError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validator
|
|
66
|
+
if issue?
|
|
67
|
+
IssueValidator.new(parsed_output)
|
|
68
|
+
elsif measurement?
|
|
69
|
+
MeasurementValidator.new(parsed_output)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module CC
|
|
2
|
+
module Analyzer
|
|
3
|
+
class EngineOutputFilter
|
|
4
|
+
ISSUE_TYPE = "issue".freeze
|
|
5
|
+
|
|
6
|
+
def initialize(config = {})
|
|
7
|
+
@config = config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def filter?(output)
|
|
11
|
+
output.blank? || (output.issue? && ignore_issue?(output.as_issue))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def ignore_issue?(issue)
|
|
17
|
+
check_disabled?(issue) || ignore_fingerprint?(issue)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def check_disabled?(issue)
|
|
21
|
+
!check_config(issue.check_name).fetch("enabled", true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def ignore_fingerprint?(issue)
|
|
25
|
+
@config.fetch("exclude_fingerprints", []).include?(issue.fingerprint)
|
|
26
|
+
rescue SourceExtractor::InvalidLocation
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_config(check_name)
|
|
31
|
+
@checks ||= @config.fetch("checks", {})
|
|
32
|
+
@checks.fetch(check_name, {})
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module CC
|
|
2
|
+
module Analyzer
|
|
3
|
+
class EngineOutputOverrider
|
|
4
|
+
def initialize(config = {})
|
|
5
|
+
@config = config
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def apply(output)
|
|
9
|
+
if output.issue?
|
|
10
|
+
override_severity(output.as_issue.as_json)
|
|
11
|
+
else
|
|
12
|
+
output
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :config
|
|
19
|
+
|
|
20
|
+
def override_severity(issue)
|
|
21
|
+
issue.merge(override("severity"))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def override(name)
|
|
25
|
+
config.
|
|
26
|
+
fetch("issue_override", {}).
|
|
27
|
+
slice(name)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module CC
|
|
2
|
+
module Analyzer
|
|
3
|
+
class Filesystem
|
|
4
|
+
attr_reader :root
|
|
5
|
+
|
|
6
|
+
def initialize(root)
|
|
7
|
+
@root = root
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def exist?(path)
|
|
11
|
+
File.exist?(path_for(path))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def source_buffer_for(path)
|
|
15
|
+
SourceBuffer.new(path, read_path(path))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def read_path(path)
|
|
19
|
+
File.read(path_for(path))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def write_path(path, content)
|
|
23
|
+
File.write(path_for(path), content)
|
|
24
|
+
File.chown(root_uid, root_gid, path_for(path))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def ls
|
|
28
|
+
Dir.entries(root).reject { |entry| [".", ".."].include?(entry) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def path_for(path)
|
|
34
|
+
File.join(root, path)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def root_uid
|
|
38
|
+
root_stat.uid
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def root_gid
|
|
42
|
+
root_stat.gid
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def root_stat
|
|
46
|
+
@root_stat ||= File.stat(root)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module CC
|
|
2
|
+
module Analyzer
|
|
3
|
+
module Formatters
|
|
4
|
+
class Formatter
|
|
5
|
+
def initialize(filesystem, output = $stdout)
|
|
6
|
+
@filesystem = filesystem
|
|
7
|
+
@output = output
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def write(data)
|
|
11
|
+
json = JSON.parse(data)
|
|
12
|
+
json["engine_name"] = current_engine.name
|
|
13
|
+
|
|
14
|
+
case json["type"].downcase
|
|
15
|
+
when "issue"
|
|
16
|
+
issues << json
|
|
17
|
+
when "warning"
|
|
18
|
+
warnings << json
|
|
19
|
+
when "measurement"
|
|
20
|
+
measurements << json
|
|
21
|
+
else
|
|
22
|
+
raise "Invalid type found: #{json["type"]}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def started
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def engine_running(engine)
|
|
30
|
+
@current_engine = engine
|
|
31
|
+
yield
|
|
32
|
+
ensure
|
|
33
|
+
@current_engine = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def finished
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def close
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def failed(_output)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
InvalidFormatterError = Class.new(StandardError)
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :current_engine
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|