sastbox_sdk 1.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.
@@ -0,0 +1,112 @@
1
+ require 'digest'
2
+
3
+ module SastBox
4
+ module Reporter
5
+ module Sarif
6
+
7
+ def generate_sarif_report
8
+ new_sarif_log
9
+ @issues.each do |issue|
10
+ sarif_result = convert_to_sarif_result(issue)
11
+ @sarif_results << sarif_result unless sarif_result.nil?
12
+ end
13
+
14
+ begin
15
+ JSON.pretty_generate(@sarif_log)
16
+ rescue JSON::GeneratorError => e
17
+ print_error("Could not generate sarif result=> #{e}")
18
+ end
19
+ end
20
+
21
+ def new_sarif_log
22
+ @sarif_results = []
23
+ @sarif_rules = []
24
+ @sarif_log = {
25
+ 'version': '2.1.0',
26
+ '$schema': 'https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json',
27
+ 'runs': sarif_runs
28
+ }
29
+ end
30
+
31
+ def sarif_runs
32
+ [{
33
+ 'tool': { 'driver': {'name': @name, 'informationUri': '', 'semanticVersion': @version, 'rules': @sarif_rules } },
34
+ 'results': @sarif_results
35
+ }]
36
+ end
37
+
38
+ def make_rule(issue)
39
+ rule_id = "#{@name}-#{Digest::SHA1.hexdigest(issue[:title])}"
40
+ rule_name = issue[:title]
41
+ help_uri = ''
42
+ help_uri = issue[:references].first if issue[:references].length > 0
43
+
44
+ rule_index = @sarif_rules.index { |r| r[:id] == rule_id }
45
+ if rule_index.nil?
46
+ rule = {
47
+ id: rule_id,
48
+ name: rule_name,
49
+ shortDescription: {text: issue[:title]},
50
+ fullDescription: {text: issue[:title]},
51
+ helpUri: help_uri,
52
+ help: {text: ''}
53
+ }
54
+ rule_index = @sarif_rules.length
55
+ @sarif_rules << rule
56
+
57
+ end
58
+ rule_index
59
+ end
60
+
61
+ def convert_to_sarif_result(issue)
62
+ rule_index = make_rule(issue)
63
+ rule = @sarif_rules[rule_index]
64
+
65
+ relative_path = filename_relative(issue[:filename])
66
+ return nil if relative_path.nil?
67
+
68
+ snippet = issue[:snippet]
69
+
70
+ sarif_result = {
71
+ ruleId: rule[:id],
72
+ ruleIndex: rule_index,
73
+ level: 'warning',
74
+ message: {text: issue[:description]},
75
+
76
+ locations: [{
77
+ physicalLocation: {
78
+ artifactLocation: {uri: relative_path, uriBaseId: '%SRCROOT%'},
79
+ region: {
80
+ snippet: {
81
+ text: snippet[:evidence_line][:content]
82
+ },
83
+ startLine: snippet[:evidence_line][:start_line]
84
+ },
85
+ contextRegion: {
86
+ snippet: {
87
+ text: snippet[:evidence_full][:content]
88
+ },
89
+ startLine: snippet[:evidence_full][:start_line],
90
+ endLine: snippet[:evidence_full][:end_line]
91
+ }
92
+ }
93
+ }],
94
+ partialFingerprints: {
95
+ hashIssueV1: issue[:hash_issue], # compatible with sastbox v1
96
+ hashIssueV2: issue[:hash_issue_v2],
97
+ snippetHashLine: snippet[:evidence_line][:hash],
98
+ snippetHashFull: snippet[:evidence_full][:hash]
99
+ },
100
+ properties: {
101
+ cweId: issue[:cwe_id].to_i,
102
+ tags: issue[:tags],
103
+ issueSeverity: issue[:severity],
104
+ }
105
+ }
106
+ sarif_result
107
+ end
108
+
109
+ end
110
+ end
111
+ end
112
+
@@ -0,0 +1,55 @@
1
+ require 'timeout'
2
+
3
+ module SastBox
4
+ module Runner
5
+
6
+ # TODO: find a better way to do this
7
+ def command?(name)
8
+ `which #{name}`
9
+ $?.success?
10
+ end
11
+
12
+ def run_cmd(cmd)
13
+ print_debug(cmd) if @opts.verbose
14
+ if command?(cmd[0])
15
+ run_cmd_with_timeout(cmd)
16
+ else
17
+ print_error("Command not found: #{cmd[0]}")
18
+ exit 1
19
+ end
20
+ end
21
+
22
+ def run_cmd_with_timeout(cmd)
23
+ out_reader = ''
24
+ err_reader = ''
25
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
26
+ # https://stackoverflow.com/questions/8952043/how-to-fix-hanging-popen3-in-ruby
27
+ stdin.close_write
28
+ output, pid = [], wait_thr.pid
29
+ begin
30
+ Timeout.timeout(@opts.timeout) do
31
+ begin
32
+ err_reader = Thread.new { stderr.read }
33
+ rescue IOError
34
+ end
35
+
36
+ out_reader = stdout.read
37
+
38
+ #output = [stdout.read, stderr.read]
39
+ Process.wait(pid)
40
+ end
41
+ rescue Errno::ECHILD
42
+ rescue Timeout::Error
43
+ print_error('Timed out - Skipping...')
44
+ Process.kill('HUP', pid)
45
+ exit 1
46
+ end
47
+ [out_reader, wait_thr.value]
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+
54
+
55
+
@@ -0,0 +1,152 @@
1
+ module SastBox
2
+ class Scanner
3
+
4
+ include SastBox::OptParser
5
+ include SastBox::Printer
6
+ include SastBox::Runner
7
+ include SastBox::Snippet
8
+ include SastBox::Reporter::Sarif
9
+ include SastBox::Cwe
10
+ include SastBox::SeverityCalculator
11
+
12
+ attr_accessor :issues
13
+
14
+ def initialize(params)
15
+ @name = params[:name]
16
+ @name_alias = params[:name_alias] || @name
17
+ @description = params[:description]
18
+ @support = params[:support]
19
+ @version = params[:version]
20
+ @tool = params[:tool]
21
+
22
+ @issues = []
23
+ end
24
+
25
+ def info
26
+ {
27
+ name: @name,
28
+ description: @description,
29
+ support: @support,
30
+ version: @version,
31
+ sdk_version: SastBox::SDK_VERSION
32
+ }
33
+ end
34
+
35
+ def add_hash_issue_v1(issue) # compatibility with sastbox v1
36
+ issue[:hash_issue] = ''
37
+ return if got_line_range?(issue) # hash v1 not needed for scanners which report start/end line
38
+
39
+ scanner_name = @name_alias
40
+ short_filename = issue[:filename].sub(@opts.codebase, '')
41
+ data = [
42
+ scanner_name,
43
+ issue[:title],
44
+ issue[:description],
45
+ short_filename,
46
+ issue[:line].to_s,
47
+ (issue[:snippet][:evidence_line][:hash] || ''),
48
+ ]
49
+ issue[:hash_issue] = Digest::SHA256.hexdigest(data.join(':'))
50
+ end
51
+
52
+ def add_hash_issue_v2(issue)
53
+ filename_path = filename_relative(issue[:filename])
54
+ filename_path = '' if filename_path.nil?
55
+ data = [
56
+ issue[:title],
57
+ issue[:description],
58
+ filename_path,
59
+ got_line_range?(issue) ? "#{issue[:start_line].to_s}-#{issue[:end_line].to_s}" : issue[:line].to_s,
60
+ (issue[:snippet][:evidence_line][:hash]|| ''),
61
+ ]
62
+ issue[:hash_issue_v2] = Digest::SHA256.hexdigest(data.join(':'))
63
+ end
64
+
65
+ def got_line_range?(issue)
66
+ issue.key?(:start_line) && issue.key?(:end_line)
67
+ end
68
+
69
+ def add_issue(issue)
70
+ return if skip_issue?(issue)
71
+ issue[:tags] = [] unless issue.key? :tags
72
+ add_severity(issue)
73
+ add_hash_issue_v1(issue)
74
+ add_hash_issue_v2(issue)
75
+ guess_cwe(issue)
76
+ @issues << issue
77
+ end
78
+
79
+ def skip_issue?(issue)
80
+ return true if issue[:filename].include?('/.git/')
81
+ return true if issue[:snippet][:read_success] == false
82
+ return false # valid issue
83
+ end
84
+
85
+ def validate_opts
86
+ enable_color(@opts.color)
87
+ if @opts.info
88
+ puts JSON.pretty_generate(info)
89
+ exit 0
90
+ end
91
+
92
+ if @opts.output.nil?
93
+ print_error('output (-o) not passed')
94
+ exit 0
95
+ end
96
+
97
+ if @opts.codebase.nil?
98
+ print_error('codebase (-c) not passed')
99
+ exit 0
100
+ end
101
+ end
102
+
103
+ def start_scan
104
+ validate_opts
105
+
106
+ print_title("Running #{@name}")
107
+ run
108
+ #finish_scan
109
+ end
110
+
111
+ def finish_scan
112
+ status = 0
113
+ status = 1 unless @issues.empty?
114
+ print_title("Finished #{@name}")
115
+ exit status
116
+ end
117
+
118
+ def save_scan_output
119
+ File.open(@opts.output, "wb") { |file| file.write(generate_sarif_report) }
120
+ print_normal("Sarif result saved to #{@opts.output}", 1)
121
+ end
122
+
123
+ def gen_random_tmp_filename(suffix = '')
124
+ File.join(Dir.tmpdir, "#{SecureRandom.urlsafe_base64}#{suffix}")
125
+ end
126
+
127
+ def gen_random_tmp_filename_json
128
+ gen_random_tmp_filename('.json')
129
+ end
130
+
131
+ def parse_json_from_str(s)
132
+ content = nil
133
+ unless s.nil?
134
+ begin
135
+ content = JSON.parse(s)
136
+ rescue JSON::ParserError
137
+ end
138
+ end
139
+ content
140
+ end
141
+
142
+ def parse_json_from_file(filename)
143
+ content = nil
144
+ if File.exist?(filename)
145
+ content = parse_json_from_str(File.read(filename))
146
+ end
147
+ content
148
+ end
149
+
150
+ end
151
+ end
152
+
@@ -0,0 +1,82 @@
1
+ module SastBox
2
+ module SeverityCalculator
3
+
4
+ def add_severity(issue)
5
+ accepted_levels = [:info, :low, :medium, :high, :critical]
6
+
7
+ if issue.key?(:severity)
8
+ issue[:severity] = issue[:severity].to_s.downcase.to_sym
9
+
10
+ unless accepted_levels.include?(issue[:severity])
11
+ issue[:severity] = attempt_to_determine_severity(issue)
12
+ end
13
+ else
14
+ issue[:severity] = attempt_to_determine_severity(issue)
15
+ end
16
+
17
+ end
18
+
19
+ def severity_pattern_found?(patterns, text)
20
+ patterns.each do |pattern|
21
+ return true if text.include?(pattern)
22
+ end
23
+ false
24
+ end
25
+
26
+ def attempt_to_determine_severity(issue)
27
+ text = "#{issue[:title]} #{issue[:description]}".downcase.gsub(/[^a-z ]/, ' ')
28
+
29
+ level = :undefined
30
+
31
+ critical = ['command exec', 'cmd exec', 'command inj', 'cmd inj', 'code inj', 'code exec',
32
+ 'file inclusion', 'dangerous send', 'insecure deserialization']
33
+
34
+ high = ['xss', 'cross site scripting', 'sqli', 'sql injection', 'insecure url',
35
+ 'file manipulation', 'file access', 'file disclosure', 'idor', 'xpath inj',
36
+ 'weak cipher', 'weak crypto', 'insecure cipher', 'insecure crypto', 'insecure encryption',
37
+ 'broken cipher', 'broken crypto', 'weak hash', 'insecure hash', 'broken hash',
38
+ 'pathtraversal', 'path traversal', 'xxe', 'xml external entities']
39
+
40
+ medium = ['mass assignment', 'secret keyword',
41
+ 'hard coded pass', 'hardcoded pass', 'pass hardcoded', 'password hardcoded',
42
+ 'password hard coded', 'secret hardcoded',
43
+ 'session fixation', 'security misconfiguration', 'vulnerable component',
44
+ 'header injection', 'csrf', 'cross site request forgery',
45
+ 'toctou', 'session setting', 'response splitting'
46
+ ]
47
+
48
+ low = ['open redirect', 'resource leak', 'format validation',
49
+ 'information disclosure', 'logging', 'logger', 'stacktrace', 'stack trace',
50
+ 'null pointer deref']
51
+
52
+ info = []
53
+
54
+
55
+ if level == :undefined
56
+ level = :critical if severity_pattern_found?(critical, text)
57
+ end
58
+
59
+ if level == :undefined
60
+ level = :high if severity_pattern_found?(high, text)
61
+ end
62
+
63
+ if level == :undefined
64
+ level = :medium if severity_pattern_found?(medium, text)
65
+ end
66
+
67
+ if level == :undefined
68
+ level = :low if severity_pattern_found?(low, text)
69
+ end
70
+
71
+ if level == :undefined
72
+ level = :info if severity_pattern_found?(info, text)
73
+ end
74
+
75
+ level
76
+ end
77
+ end
78
+ end
79
+
80
+
81
+
82
+
@@ -0,0 +1,107 @@
1
+ require 'digest'
2
+
3
+ module SastBox
4
+ module Snippet
5
+
6
+ def filename_relative(filename)
7
+ #filename.sub(@opts.codebase, '') if filename.start_with?(@opts.codebase)
8
+ filename_path = File.expand_path(filename)
9
+ codebase_path = File.expand_path(@opts.codebase)
10
+
11
+ if filename_path.start_with?(codebase_path)
12
+ filename_path.sub!(codebase_path, '')
13
+ filename_path = filename_path[1..-1] if filename_path.start_with?('/')
14
+ return filename_path
15
+ else
16
+ #print_warning("Filename outside codebase => #{filename_path}")
17
+ return nil
18
+ end
19
+ end
20
+
21
+ def snippet_calculate_hashes(snippet)
22
+ snippet[:evidence_line][:hash] = Digest::SHA256.hexdigest(snippet[:evidence_line][:content])
23
+ snippet[:evidence_full][:hash] = Digest::SHA256.hexdigest(snippet[:evidence_full][:content])
24
+ end
25
+
26
+ def snippet_read(filename, line, context=5)
27
+ snippet = {
28
+ evidence_line: { content: '', start_line: 0, end_line: 0, hash: '' },
29
+ evidence_full: { content: '', start_line: 0, end_line: 0, hash: '' },
30
+ read_success: false
31
+ }
32
+
33
+ if File.file?(filename)
34
+ snippet[:read_success] = true
35
+ lines = File.open(filename).readlines
36
+ begin_code = [1, line.to_i - context].max
37
+ end_code = [line.to_i + context, lines.length].min
38
+
39
+ if end_code > lines.length or line.to_i > lines.length
40
+ snippet[:read_success] = false
41
+ return snippet
42
+ end
43
+
44
+ snippet[:evidence_line][:start_line] = line.to_i
45
+ snippet[:evidence_line][:end_line] = line.to_i
46
+ snippet[:evidence_line][:content] = lines[line.to_i - 1].chomp.force_encoding('ISO-8859-1').encode('UTF-8')
47
+
48
+ snippet[:evidence_full][:start_line] = begin_code
49
+ snippet[:evidence_full][:end_line] = end_code
50
+
51
+ begin_code.upto(end_code) do |pos|
52
+ snippet[:evidence_full][:content] << lines[pos - 1].force_encoding('ISO-8859-1').encode('UTF-8')
53
+ end
54
+ snippet_calculate_hashes(snippet)
55
+ #snippet[:evidence_line][:hash] = Digest::SHA256.hexdigest(snippet[:evidence_line][:content])
56
+ #snippet[:evidence_full][:hash] = Digest::SHA256.hexdigest(snippet[:evidence_full][:content])
57
+ end
58
+
59
+ snippet
60
+ end
61
+
62
+ def snippet_read_range(filename, start_line, end_line, context=5)
63
+ snippet = {
64
+ evidence_line: { content: '', start_line: 0, end_line: 0, hash: '' },
65
+ evidence_full: { content: '', start_line: 0, end_line: 0, hash: '' },
66
+ read_success: false
67
+ }
68
+
69
+ if File.file?(filename)
70
+ snippet[:read_success] = true
71
+ lines = File.open(filename).readlines
72
+ num_lines = lines.length
73
+
74
+ if !start_line.between?(1, num_lines) || !end_line.between?(1, num_lines) || start_line > end_line
75
+ snippet[:read_success] = false
76
+ return snippet
77
+ end
78
+
79
+ begin_code = [1, start_line.to_i - context].max
80
+ end_code = [end_line.to_i + context, num_lines].min
81
+
82
+ if end_code > num_lines
83
+ snippet[:read_success] = false
84
+ return snippet
85
+ end
86
+
87
+ snippet[:evidence_line][:start_line] = start_line.to_i
88
+ snippet[:evidence_line][:end_line] = end_line.to_i
89
+
90
+ start_line.upto(end_line) do |pos|
91
+ snippet[:evidence_line][:content] << lines[pos - 1].force_encoding('ISO-8859-1').encode('UTF-8')
92
+ end
93
+
94
+ snippet[:evidence_full][:start_line] = begin_code
95
+ snippet[:evidence_full][:end_line] = end_code
96
+
97
+ begin_code.upto(end_code) do |pos|
98
+ snippet[:evidence_full][:content] << lines[pos - 1].force_encoding('ISO-8859-1').encode('UTF-8')
99
+ end
100
+
101
+ snippet_calculate_hashes(snippet)
102
+ end
103
+
104
+ snippet
105
+ end
106
+ end
107
+ end