FOSSology  4.4.0
Open Source License Compliance by Open Source Software
fossologyscanner.py
1 #!/usr/bin/env python3
2 
3 # SPDX-FileCopyrightText: © 2020,2023 Siemens AG
4 # SPDX-FileCopyrightText: © anupam.ghosh@siemens.com
5 # SPDX-FileCopyrightText: © mishra.gaurav@siemens.com
6 
7 # SPDX-License-Identifier: GPL-2.0-only
8 
9 import argparse
10 import json
11 import os
12 import sys
13 import textwrap
14 from typing import List, Union, IO
15 
16 from FoScanner.ApiConfig import (ApiConfig, Runner)
17 from FoScanner.CliOptions import (CliOptions, ReportFormat)
18 from FoScanner.RepoSetup import RepoSetup
19 from FoScanner.Scanners import (Scanners, ScanResult)
20 from FoScanner.SpdxReport import SpdxReport
21 
22 
23 def get_api_config() -> ApiConfig:
24  """
25  Set the API configuration based on CI the job is running on
26 
27  :return: ApiConfig object
28  """
29  api_config = ApiConfig()
30  if 'GITLAB_CI' in os.environ:
31  api_config.running_on = Runner.GITLAB
32  api_config.api_url = os.environ['CI_API_V4_URL'] if 'CI_API_V4_URL' in \
33  os.environ else ''
34  api_config.project_id = os.environ['CI_PROJECT_ID'] if 'CI_PROJECT_ID' in \
35  os.environ else ''
36  api_config.mr_iid = os.environ['CI_MERGE_REQUEST_IID'] if \
37  'CI_MERGE_REQUEST_IID' in os.environ else ''
38  api_config.api_token = os.environ['API_TOKEN'] if 'API_TOKEN' in \
39  os.environ else ''
40  api_config.project_name = os.environ['CI_PROJECT_NAME'] if \
41  'CI_PROJECT_NAME' in os.environ else ''
42  api_config.project_desc = os.environ['CI_PROJECT_DESCRIPTION'].strip()
43  if api_config.project_desc == "":
44  api_config.project_desc = None
45  api_config.project_orig = os.environ['CI_PROJECT_NAMESPACE']
46  api_config.project_url = os.environ['CI_PROJECT_URL']
47  elif 'TRAVIS' in os.environ and os.environ['TRAVIS'] == 'true':
48  api_config.running_on = Runner.TRAVIS
49  api_config.travis_repo_slug = os.environ['TRAVIS_REPO_SLUG']
50  api_config.travis_pull_request = os.environ['TRAVIS_PULL_REQUEST']
51  api_config.project_name = os.environ['TRAVIS_REPO_SLUG'].split("/")[-1]
52  api_config.project_orig = "/".join(os.environ['TRAVIS_REPO_SLUG'].
53  split("/")[:-2])
54  api_config.project_url = "https://github.com/" + \
55  os.environ['TRAVIS_REPO_SLUG']
56  elif 'GITHUB_ACTIONS' in os.environ and \
57  os.environ['GITHUB_ACTIONS'] == 'true':
58  api_config.running_on = Runner.GITHUB
59  api_config.api_url = os.environ['GITHUB_API'] if 'GITHUB_API' in \
60  os.environ else 'https://api.github.com'
61  api_config.api_token = os.environ['GITHUB_TOKEN']
62  api_config.github_repo_slug = os.environ['GITHUB_REPOSITORY']
63  api_config.github_pull_request = os.environ['GITHUB_PULL_REQUEST']
64  api_config.project_name = os.environ['GITHUB_REPOSITORY'].split("/")[-1]
65  api_config.project_orig = os.environ['GITHUB_REPO_OWNER']
66  api_config.project_url = os.environ['GITHUB_REPO_URL']
67  return api_config
68 
69 
70 def get_allow_list() -> dict:
71  """
72  Decode json from `allowlist.json`
73 
74  :return: allowlist dictionary
75  """
76  if os.path.exists('whitelist.json'):
77  file_name = 'whitelist.json'
78  else:
79  file_name = 'allowlist.json'
80  with open(file_name) as f:
81  data = json.load(f)
82  return data
83 
84 
85 def print_results(name: str, failed_results: List[ScanResult],
86  result_file: IO):
87  """
88  Print the formatted scanner results
89 
90  :param name: Name of the scanner
91  :param failed_results: formatted scanner results to be printed
92  :param result_file: File to write results to
93  """
94  for files in failed_results:
95  print(f"File: {files.file}")
96  result_file.write(f"File: {files.file}\n")
97  plural = ""
98  if len(files.result) > 1:
99  plural = "s"
100  print(f"{name}{plural}:")
101  result_file.write(f"{name}{plural}:\n")
102  for result in files.result:
103  print("\t" + result)
104  result_file.write("\t" + result + "\n")
105 
106 
107 def print_log_message(filename: str,
108  failed_list: Union[bool, List[ScanResult]],
109  check_value: bool, failure_text: str,
110  acceptance_text: str, scan_type: str,
111  return_val: int) -> int:
112  """
113  Common helper function to print scan results.
114 
115  :param filename: File where results are to be stored.
116  :param failed_list: Failed scan results.
117  :param check_value: Boolean value which failed_list should have.
118  :param failure_text: Message to print in case of failures.
119  :param acceptance_text: Message to print in case of no failures.
120  :param scan_type: Type of scan to print.
121  :param return_val: Return value for program
122  :return: New return value
123  """
124  report_file = open(filename, 'w')
125  if (isinstance(failed_list, bool) and failed_list is not check_value) or \
126  (isinstance(failed_list, list) and len(failed_list) != 0):
127  print(f"\u2718 {failure_text}:")
128  report_file.write(f"{failure_text}:\n")
129  print_results(scan_type, failed_list, report_file)
130  if scan_type == "License":
131  return_val = return_val | 2
132  elif scan_type == "Copyright":
133  return_val = return_val | 4
134  elif scan_type == "Keyword":
135  return_val = return_val | 8
136  else:
137  print(f"\u2714 {acceptance_text}")
138  report_file.write(f"{acceptance_text}\n")
139  print()
140  report_file.close()
141  return return_val
142 
143 
144 def text_report(cli_options: CliOptions, result_dir: str, return_val: int,
145  scanner: Scanners) -> int:
146  """
147  Run scanners and print results in text format.
148 
149  :param cli_options: CLI options
150  :param result_dir: Result directory location
151  :param return_val: Return value of program
152  :param scanner: Scanner object
153  :return: Program's return value
154  """
155  if cli_options.nomos or cli_options.ojo:
156  failed_licenses = scanner.results_are_allow_listed()
157  print_log_message(f"{result_dir}/licenses.txt", failed_licenses, True,
158  "Following licenses found which are not allow listed",
159  "No license violation found", "License", return_val)
160  if cli_options.copyright:
161  copyright_results = scanner.get_copyright_list()
162  print_log_message(f"{result_dir}/copyrights.txt", copyright_results, False,
163  "Following copyrights found",
164  "No copyright violation found", "Copyright", return_val)
165  if cli_options.keyword:
166  keyword_results = scanner.get_keyword_list()
167  print_log_message(f"{result_dir}/keywords.txt", keyword_results, False,
168  "Following keywords found",
169  "No keyword violation found", "Keyword", return_val)
170  return return_val
171 
172 
173 def bom_report(cli_options: CliOptions, result_dir: str, return_val: int,
174  scanner: Scanners, api_config: ApiConfig) -> int:
175  """
176  Run scanners and print results as an SBOM.
177 
178  :param cli_options: CLI options
179  :param result_dir: Result directory location
180  :param return_val: Return value
181  :param scanner: Scanner object
182  :param api_config: API config options
183  :return: Program's return value
184  """
185  report_obj = SpdxReport(cli_options, api_config)
186  if cli_options.nomos or cli_options.ojo:
187  scan_results = scanner.get_scanner_results()
188  report_obj.add_license_results(scan_results)
189  failed_licenses = scanner.get_non_allow_listed_results(scan_results)
190  return_val = print_log_message(f"{result_dir}/licenses.txt",
191  failed_licenses, True, "Following licenses found which are not allow "
192  "listed", "No license violation found",
193  "License", return_val)
194  if cli_options.copyright:
195  copyright_results = scanner.get_copyright_list(all_results=True)
196  if copyright_results is False:
197  copyright_results = []
198  report_obj.add_copyright_results(copyright_results)
199  failed_copyrights = scanner.get_non_allow_listed_copyrights(
200  copyright_results)
201  return_val = print_log_message(f"{result_dir}/copyrights.txt",
202  failed_copyrights, False, "Following copyrights found",
203  "No copyright violation found", "Copyright", return_val)
204  if cli_options.keyword:
205  keyword_results = scanner.get_keyword_list()
206  return_val = print_log_message(f"{result_dir}/keywords.txt",
207  keyword_results, False, "Following keywords found",
208  "No keyword violation found", "Keyword", return_val)
209  report_obj.finalize_document()
210  report_name = f"{result_dir}/sbom_"
211  if cli_options.report_format == ReportFormat.SPDX_JSON:
212  report_name += "spdx.json"
213  elif cli_options.report_format == ReportFormat.SPDX_RDF:
214  report_name += "spdx.rdf"
215  elif cli_options.report_format == ReportFormat.SPDX_TAG_VALUE:
216  report_name += "spdx.spdx"
217  elif cli_options.report_format == ReportFormat.SPDX_YAML:
218  report_name += "spdx.yaml"
219  report_obj.write_report(report_name)
220  print(f"\u2714 Saved SBOM as {report_name}")
221  return return_val
222 
223 
224 def main(parsed_args):
225  """
226  Main
227 
228  :param parsed_args:
229  :return: 0 for success, error code on failure.
230  """
231  api_config = get_api_config()
232  cli_options = CliOptions()
233  cli_options.update_args(parsed_args)
234 
235  try:
236  cli_options.allowlist = get_allow_list()
237  except FileNotFoundError:
238  print("Unable to find allowlist.json in current dir\n"
239  "Continuing without it.", file=sys.stderr)
240 
241  repo_setup = RepoSetup(cli_options, api_config)
242  if cli_options.repo is False:
243  cli_options.diff_dir = repo_setup.get_diff_dir()
244 
245  scanner = Scanners(cli_options)
246  return_val = 0
247 
248  # Create result dir
249  result_dir = "results"
250  os.makedirs(name=result_dir, exist_ok=True)
251 
252  if cli_options.report_format == ReportFormat.TEXT:
253  return_val = text_report(cli_options, result_dir, return_val, scanner)
254  else:
255  return_val = bom_report(cli_options, result_dir, return_val, scanner,
256  api_config)
257  return return_val
258 
259 
260 if __name__ == "__main__":
261  parser = argparse.ArgumentParser(
262  description=textwrap.dedent("""fossology scanner designed for CI""")
263  )
264  parser.add_argument(
265  "operation", type=str, help="Operations to run.", nargs='*',
266  choices=["nomos", "copyright", "keyword", "ojo", "repo"]
267  )
268  parser.add_argument(
269  "--report", type=str, help="Type of report to generate. Default 'TEXT'.",
270  choices=[member.name for member in ReportFormat], default=ReportFormat.TEXT.name
271  )
272  args = parser.parse_args()
273  sys.exit(main(args))
274 
Store the options sent through the CLI.