|
1 | 1 | # -*- coding: utf_8 -*-
|
2 |
| -"""Sarif output format. |
| 2 | +"""SARIF output formatter for MobSF scan results. |
3 | 3 |
|
4 |
| -Based on https://github.com/microsoft/bandit-sarif-formatter/ |
5 |
| -blob/master/bandit_sarif_formatter/formatter.py |
| 4 | +Based on https://github.com/microsoft/bandit-sarif-formatter/blob/master/bandit_sarif_formatter/formatter.py |
| 5 | +MIT License, Copyright (c) Microsoft Corporation. |
6 | 6 |
|
7 |
| -Copyright (c) Microsoft. All Rights Reserved. |
8 |
| -MIT License |
9 |
| -
|
10 |
| -Copyright (c) Microsoft Corporation. |
11 |
| -
|
12 |
| -Permission is hereby granted, free of charge, to any person obtaining a copy |
13 |
| -of this software and associated documentation files (the "Software"), to deal |
14 |
| -in the Software without restriction, including without limitation the rights |
15 |
| -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
16 |
| -copies of the Software, and to permit persons to whom the Software is |
17 |
| -furnished to do so, subject to the following conditions: |
18 |
| -
|
19 |
| -The above copyright notice and this permission notice shall be included in all |
20 |
| -copies or substantial portions of the Software. |
21 |
| -
|
22 |
| -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
23 |
| -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
24 |
| -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
25 |
| -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
26 |
| -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
27 |
| -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
28 |
| -SOFTWARE |
29 | 7 | """
|
30 | 8 | from datetime import datetime
|
31 | 9 | from pathlib import PurePath
|
32 | 10 | import urllib.parse as urlparse
|
33 |
| - |
34 | 11 | import sarif_om as om
|
35 |
| - |
36 | 12 | from jschema_to_python.to_json import to_json
|
37 | 13 |
|
38 |
| - |
39 | 14 | TS_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
|
40 | 15 |
|
41 |
| - |
42 | 16 | def level_from_severity(severity):
|
43 |
| - if severity == 'ERROR': |
44 |
| - return 'error' |
45 |
| - elif severity == 'WARNING': |
46 |
| - return 'warning' |
47 |
| - elif severity == 'INFO': |
48 |
| - return 'note' |
49 |
| - else: |
50 |
| - return 'none' |
51 |
| - |
| 17 | + return { |
| 18 | + 'ERROR': 'error', |
| 19 | + 'WARNING': 'warning', |
| 20 | + 'INFO': 'note' |
| 21 | + }.get(severity, 'none') |
52 | 22 |
|
53 | 23 | def to_uri(file_path):
|
54 | 24 | pure_path = PurePath(file_path)
|
55 |
| - if pure_path.is_absolute(): |
56 |
| - return pure_path.as_uri() |
57 |
| - else: |
58 |
| - posix_path = pure_path.as_posix() # Replace backslashes with slashes. |
59 |
| - return urlparse.quote(posix_path) # %-encode special characters. |
60 |
| - |
61 |
| - |
62 |
| -def get_rule_name(rule_id): |
63 |
| - normalized = [] |
64 |
| - noms = rule_id.split('_') |
65 |
| - for nom in noms: |
66 |
| - normalized.append(nom.capitalize()) |
67 |
| - return ''.join(normalized) |
| 25 | + return pure_path.as_uri() if pure_path.is_absolute() else urlparse.quote(pure_path.as_posix()) |
68 | 26 |
|
| 27 | +def format_rule_name(rule_id): |
| 28 | + return ''.join(word.capitalize() for word in rule_id.split('_')) |
69 | 29 |
|
70 | 30 | def add_results(path, scan_results, run):
|
71 | 31 | if run.results is None:
|
72 | 32 | run.results = []
|
73 |
| - res = {} |
74 |
| - res.update(scan_results.get('results', [])) |
| 33 | + res = scan_results.get('results', {}) |
75 | 34 | rules = {}
|
76 | 35 | rule_indices = {}
|
77 | 36 |
|
78 | 37 | for rule_id, issue_dict in res.items():
|
79 |
| - results = create_rule_results(path, rule_id, issue_dict, rules, rule_indices) |
80 |
| - run.results += results |
| 38 | + rule_results = create_rule_results(path, rule_id, issue_dict, rules, rule_indices) |
| 39 | + run.results.extend(rule_results) |
81 | 40 |
|
82 |
| - if len(rules) > 0: |
| 41 | + if rules: |
83 | 42 | run.tool.driver.rules = list(rules.values())
|
84 | 43 |
|
85 |
| - |
86 | 44 | def create_rule_results(path, rule_id, issue_dict, rules, rule_indices):
|
87 | 45 | rule_results = []
|
88 |
| - if rule_id in rules: |
89 |
| - rule = rules[rule_id] |
90 |
| - rule_index = rule_indices[rule_id] |
91 |
| - else: |
92 |
| - doc = issue_dict['metadata'].get('reference') |
93 |
| - if not doc: |
94 |
| - doc = ('https://mobile-security.gitbook.io/' |
95 |
| - 'mobile-security-testing-guide/') |
| 46 | + rule, rule_index = rules.get(rule_id), rule_indices.get(rule_id) |
| 47 | + |
| 48 | + if not rule: |
| 49 | + doc = issue_dict['metadata'].get('reference') or 'https://mobile-security.gitbook.io/mobile-security-testing-guide/' |
96 | 50 | cwe_id = issue_dict['metadata']['cwe'].split(':')[0].lower()
|
97 | 51 | rule = om.ReportingDescriptor(
|
98 | 52 | id=rule_id,
|
99 |
| - name=get_rule_name(rule_id), |
| 53 | + name=format_rule_name(rule_id), |
100 | 54 | help_uri=doc,
|
101 |
| - properties={ |
102 |
| - 'tags': ['security', f'external/cwe/{cwe_id}'], |
103 |
| - }, |
| 55 | + properties={'tags': ['security', f'external/cwe/{cwe_id}']} |
104 | 56 | )
|
105 | 57 | rule_index = len(rules)
|
106 | 58 | rules[rule_id] = rule
|
107 | 59 | rule_indices[rule_id] = rule_index
|
108 | 60 |
|
109 |
| - files = issue_dict.get('files', []) |
110 |
| - |
111 |
| - # if there are locations - we iterate over them and create |
112 |
| - # a separete result for each location |
113 |
| - if files: |
114 |
| - for item in files: |
115 |
| - locations = [] |
116 |
| - physical_location = om.PhysicalLocation( |
117 |
| - artifact_location=om.ArtifactLocation( |
118 |
| - uri=to_uri(item['file_path'])), |
| 61 | + for item in issue_dict.get('files', []): |
| 62 | + location = create_location(item) |
| 63 | + rule_results.append(create_result(rule, rule_index, issue_dict, [location])) |
| 64 | + |
| 65 | + if not issue_dict.get('files'): |
| 66 | + default_location = om.Location( |
| 67 | + physical_location=om.PhysicalLocation( |
| 68 | + artifact_location=om.ArtifactLocation(uri=path[0]), |
| 69 | + region=om.Region( |
| 70 | + start_line=1, |
| 71 | + end_line=1, |
| 72 | + start_column=1, |
| 73 | + end_column=1, |
| 74 | + snippet=om.ArtifactContent(text='Missing Best Practice') |
| 75 | + ) |
119 | 76 | )
|
120 |
| - physical_location.region = om.Region( |
| 77 | + ) |
| 78 | + rule_results.append(create_result(rule, rule_index, issue_dict, [default_location])) |
| 79 | + |
| 80 | + return rule_results |
| 81 | + |
| 82 | +def create_location(item): |
| 83 | + return om.Location( |
| 84 | + physical_location=om.PhysicalLocation( |
| 85 | + artifact_location=om.ArtifactLocation(uri=to_uri(item['file_path'])), |
| 86 | + region=om.Region( |
121 | 87 | start_line=item['match_lines'][0],
|
122 | 88 | end_line=item['match_lines'][1],
|
123 | 89 | start_column=item['match_position'][0],
|
124 | 90 | end_column=item['match_position'][1],
|
125 |
| - snippet=om.ArtifactContent(text=item['match_string']), |
| 91 | + snippet=om.ArtifactContent(text=item['match_string']) |
126 | 92 | )
|
127 |
| - locations.append(om.Location(physical_location=physical_location)) |
128 |
| - rule_results.append(om.Result( |
129 |
| - rule_id=rule.id, |
130 |
| - rule_index=rule_index, |
131 |
| - message=om.Message(text=issue_dict['metadata']['description']), |
132 |
| - level=level_from_severity(issue_dict['metadata']['severity']), |
133 |
| - locations=locations, |
134 |
| - properties={ |
135 |
| - 'owasp-mobile': issue_dict['metadata']['owasp-mobile'], |
136 |
| - 'masvs': issue_dict['metadata']['masvs'], |
137 |
| - 'cwe': issue_dict['metadata']['cwe'], |
138 |
| - 'reference': issue_dict['metadata']['reference'], |
139 |
| - }, |
140 |
| - )) |
141 |
| - # if there are no locations - only create a single resuklt |
142 |
| - else: |
143 |
| - locations = [] |
144 |
| - artifact = om.PhysicalLocation( |
145 |
| - artifact_location=om.ArtifactLocation( |
146 |
| - uri=path[0]), |
147 | 93 | )
|
148 |
| - artifact.region = om.Region( |
149 |
| - start_line=1, |
150 |
| - end_line=1, |
151 |
| - start_column=1, |
152 |
| - end_column=1, |
153 |
| - snippet=om.ArtifactContent(text='Missing Best Practice'), |
154 |
| - ) |
155 |
| - locations.append(om.Location(physical_location=artifact)) |
156 |
| - rule_results.append(om.Result( |
157 |
| - rule_id=rule.id, |
158 |
| - rule_index=rule_index, |
159 |
| - message=om.Message(text=issue_dict['metadata']['description']), |
160 |
| - level=level_from_severity(issue_dict['metadata']['severity']), |
161 |
| - locations=locations, |
162 |
| - properties={ |
163 |
| - 'owasp-mobile': issue_dict['metadata']['owasp-mobile'], |
164 |
| - 'masvs': issue_dict['metadata']['masvs'], |
165 |
| - 'cwe': issue_dict['metadata']['cwe'], |
166 |
| - 'reference': issue_dict['metadata']['reference'], |
167 |
| - }, |
168 |
| - )) |
169 |
| - |
170 |
| - return rule_results |
| 94 | + ) |
171 | 95 |
|
| 96 | +def create_result(rule, rule_index, issue_dict, locations): |
| 97 | + return om.Result( |
| 98 | + rule_id=rule.id, |
| 99 | + rule_index=rule_index, |
| 100 | + message=om.Message(text=issue_dict['metadata']['description']), |
| 101 | + level=level_from_severity(issue_dict['metadata']['severity']), |
| 102 | + locations=locations, |
| 103 | + properties={ |
| 104 | + 'owasp-mobile': issue_dict['metadata']['owasp-mobile'], |
| 105 | + 'masvs': issue_dict['metadata']['masvs'], |
| 106 | + 'cwe': issue_dict['metadata']['cwe'], |
| 107 | + 'reference': issue_dict['metadata']['reference'], |
| 108 | + } |
| 109 | + ) |
172 | 110 |
|
173 | 111 | def sarif_output(outfile, scan_results, mobsfscan_version, path):
|
174 | 112 | log = om.SarifLog(
|
175 |
| - schema_uri=('https://raw.githubusercontent.com/oasis-tcs/' |
176 |
| - 'sarif-spec/master/Schemata/sarif-schema-2.1.0.json'), |
| 113 | + schema_uri='https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json', |
177 | 114 | version='2.1.0',
|
178 |
| - runs=[ |
179 |
| - om.Run( |
180 |
| - tool=om.Tool(driver=om.ToolComponent( |
181 |
| - name='mobsfscan', |
182 |
| - information_uri='https://github.com/MobSF/mobsfscan', |
183 |
| - semantic_version=mobsfscan_version, |
184 |
| - version=mobsfscan_version), |
185 |
| - ), |
186 |
| - invocations=[ |
187 |
| - om.Invocation( |
188 |
| - end_time_utc=datetime.utcnow().strftime(TS_FORMAT), |
189 |
| - execution_successful=True, |
190 |
| - ), |
191 |
| - ], |
192 |
| - ), |
193 |
| - ], |
| 115 | + runs=[om.Run( |
| 116 | + tool=om.Tool(driver=om.ToolComponent( |
| 117 | + name='mobsfscan', |
| 118 | + information_uri='https://github.com/MobSF/mobsfscan', |
| 119 | + semantic_version=mobsfscan_version, |
| 120 | + version=mobsfscan_version |
| 121 | + )), |
| 122 | + invocations=[om.Invocation( |
| 123 | + end_time_utc=datetime.utcnow().strftime(TS_FORMAT), |
| 124 | + execution_successful=True |
| 125 | + )] |
| 126 | + )] |
194 | 127 | )
|
195 | 128 | run = log.runs[0]
|
196 | 129 | add_results(path, scan_results, run)
|
197 | 130 | json_out = to_json(log)
|
| 131 | + |
198 | 132 | if outfile:
|
199 | 133 | with open(outfile, 'w') as of:
|
200 | 134 | of.write(json_out)
|
201 | 135 | else:
|
202 | 136 | print(json_out)
|
| 137 | + |
203 | 138 | return json_out
|
0 commit comments