Coverage for veracode/options.py : 100%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python3
2"""
3Config parser for easy_sast
4"""
6# built-ins
7from argparse import ArgumentParser
8import logging
9from pathlib import Path
10from typing import Any, Dict, List, Union
11import os
12import sys
14# third party
15import yaml
17# custom
18from veracode.api import is_valid_attribute
19from veracode import constants
20from veracode import __version__
22LOG = logging.getLogger(__name__)
25def remove_nones(*, obj: Any) -> Any:
26 """
27 Remove Nones from a provided object
28 """
29 if isinstance(obj, (list, tuple, set)):
30 return type(obj)(remove_nones(obj=item) for item in obj if item is not None)
31 if isinstance(obj, dict):
32 return type(obj)(
33 (remove_nones(obj=key), remove_nones(obj=value))
34 for key, value in obj.items()
35 if key is not None and value is not None
36 )
37 return obj
40def remove_empty_dicts(*, obj: Any) -> Any:
41 """
42 Remove empty dicts from a provided object
43 """
44 if isinstance(obj, (list, tuple, set)):
45 return type(obj)(remove_empty_dicts(obj=item) for item in obj if item != {})
46 if isinstance(obj, dict):
47 return type(obj)(
48 (remove_empty_dicts(obj=key), remove_empty_dicts(obj=value))
49 for key, value in obj.items()
50 if value != {}
51 )
52 return obj
55def filter_config(*, config: Dict) -> Dict:
56 """
57 Perform config filtering
58 """
59 # Perform initial filtering
60 filtered_config = remove_nones(obj=config)
61 filtered_config = remove_empty_dicts(obj=filtered_config)
63 # If filtering did not result in any changes, return the config
64 if config == filtered_config:
65 return config
67 # Perform recursive filtering
68 previous_iteration_filtered_config = filtered_config
69 while True:
70 if (filtered_config := remove_empty_dicts(obj=remove_nones(obj=filtered_config))) != previous_iteration_filtered_config:
71 previous_iteration_filtered_config = filtered_config
72 else:
73 return filtered_config
76def get_default_config() -> Dict:
77 """
78 Return a dict of the default values
79 """
80 default_config: Dict[str, Union[int, List[str]]] = {}
81 # Set the workflow default
82 default_config["workflow"] = constants.DEFAULT_WORKFLOW
83 # Set the loglevel default
84 default_config["loglevel"] = 'warning'
86 return default_config
89def get_file_config(*, config_file: Path) -> Dict:
90 """
91 Return a dict of the values provided in the config file
92 """
93 if isinstance(config_file, Path):
94 # Build the file_config
95 file_config = parse_file_config(config_file=config_file)
96 normalized_file_config = normalize_config(config=file_config)
97 else:
98 # Invalid argument
99 LOG.exception("config_file must be a Path object")
100 raise ValueError
102 return normalized_file_config
105def parse_file_config(*, config_file: Path) -> Dict:
106 """
107 Parse the sast-veracode config file
108 """
109 # Filter
110 suffix_whitelist = {".yml", ".yaml"}
112 if config_file.suffix not in suffix_whitelist:
113 LOG.error("Suffix for the config file %s is not allowed", config_file)
114 return {}
116 try:
117 with open(config_file) as yaml_data:
118 config = yaml.safe_load(yaml_data)
119 except FileNotFoundError:
120 LOG.warning("The config file %s was not found", config_file)
121 config = {}
122 except PermissionError as pe_err:
123 LOG.exception(
124 "Permission denied when attempting to read the config file %s", config_file
125 )
126 raise pe_err
127 except OSError as os_err:
128 LOG.exception(
129 "Unknown OS error when attempting to read the config file %s", config_file
130 )
131 raise os_err
133 return config
136def get_env_config() -> Dict:
137 """
138 Return a dict of the environment variables
139 """
140 env_var_config = {}
141 env_var_config["api_key_id"] = os.environ.get("VERACODE_API_KEY_ID", None)
142 env_var_config["api_key_secret"] = os.environ.get("VERACODE_API_KEY_SECRET", None)
143 return env_var_config
146def normalize_config(*, config: Dict) -> Dict:
147 """
148 Normalize a provided config dict into the preferred format for the
149 supported APIs
150 """
151 # Add the top level "apis" key
152 if "apis" not in config.keys():
153 config["apis"] = {}
155 # Add a key for each of the supported APIs under the top level "apis" key
156 for api in constants.SUPPORTED_APIS:
157 if api not in config["apis"].keys():
158 config["apis"][api] = {}
160 # Search for a loglevel value provided as a string and modify it to be an
161 # accurate level
162 if 'loglevel' in config.keys() and isinstance(config['loglevel'], str):
163 if hasattr(logging, config['loglevel']):
164 config['loglevel'] = getattr(logging, config['loglevel'])
165 else:
166 LOG.exception("Unable to normalize the provided loglevel")
167 raise AttributeError
169 # Distribute the keys into the appropriate slots
170 for upload_attribute in constants.REQUIRED_UPLOAD_ATTRIBUTES:
171 if upload_attribute in config.keys():
172 config["apis"]["upload"][upload_attribute] = config[
173 upload_attribute
174 ]
176 # Only clean up if the attribute is not also in results. This
177 # approach will need to be reconsidered as more APIs are supported
178 if upload_attribute not in constants.REQUIRED_RESULTS_ATTRIBUTES:
179 del config[upload_attribute]
181 for results_attribute in constants.REQUIRED_RESULTS_ATTRIBUTES:
182 if results_attribute in config.keys():
183 config["apis"]["results"][results_attribute] = config[
184 results_attribute
185 ]
186 # Since this is the last normalization step, cleanup without regard
187 del config[results_attribute]
189 # Filter the config
190 config = filter_config(config=config)
192 return config
195def get_args_config() -> Dict:
196 """
197 Get the config passed as an argument
198 """
199 parser = create_arg_parser()
200 parsed_args = vars(parser.parse_args())
202 normal_keys = [
203 "app_id",
204 "api_key_id",
205 "api_key_secret",
206 "build_dir",
207 "build_id",
208 "config_file",
209 "loglevel",
210 "workflow",
211 "ignore_compliance_status",
212 ]
213 disable_keys = {
214 "auto_scan": "disable_auto_scan",
215 "scan_all_nonfatal_top_level_modules": "disable_scan_nonfatal_modules",
216 }
218 # Load parsed arguments into args_config
219 args_config = {}
220 for key in normal_keys:
221 if key in parsed_args.keys():
222 args_config[key] = parsed_args[key]
224 for key, value in disable_keys.items():
225 if value in parsed_args.keys():
226 args_config[key] = not parsed_args[value]
228 normalized_args_config = normalize_config(config=args_config)
230 return normalized_args_config
233def create_arg_parser() -> ArgumentParser:
234 """Parse the arguments"""
235 parser = ArgumentParser()
236 parser.add_argument(
237 "--api-key-id", type=str, help="veracode api key id",
238 )
239 parser.add_argument(
240 "--api-key-secret", type=str, help="veracode api key secret",
241 )
242 parser.add_argument(
243 "--app-id", type=str, help="application id as provided by Veracode",
244 )
245 parser.add_argument(
246 "--build-dir",
247 type=lambda p: Path(p).absolute(),
248 help="a Path containing build artifacts",
249 )
250 parser.add_argument(
251 "--build-id", type=str, help="application build id",
252 )
253 parser.add_argument(
254 "--config-file",
255 type=lambda p: Path(p).absolute(),
256 default=Path("config.yml").absolute(),
257 help="specify a config file",
258 )
259 parser.add_argument(
260 "--disable-auto-scan", action="store_true", help="disable auto_scan"
261 )
262 parser.add_argument(
263 "--disable-scan-nonfatal-modules",
264 action="store_true",
265 help="disable scan_all_nonfatal_top_level_modules",
266 )
267 parser.add_argument(
268 "--ignore-compliance-status",
269 action="store_true",
270 help="ignore (but still check) the compliance status",
271 )
272 parser.add_argument("--version", action="version", version=__version__)
273 parser.add_argument(
274 "--workflow", nargs="+", help="specify the workflow steps to enable and order"
275 )
277 group = parser.add_mutually_exclusive_group()
278 group.add_argument(
279 "--debug",
280 action="store_const",
281 dest="loglevel",
282 const=logging.DEBUG,
283 help="enable debug level logging",
284 )
285 group.add_argument(
286 "--verbose",
287 action="store_const",
288 dest="loglevel",
289 const=logging.INFO,
290 help="enable info level logging",
291 )
292 return parser
295def get_config() -> Dict:
296 """
297 Get the config dict
298 """
299 default_config = get_default_config()
300 args_config = get_args_config()
301 file_config = get_file_config(config_file=args_config["config_file"])
302 env_config = get_env_config()
304 ## Create the final config
305 # Start with the (naive) default
306 config = default_config
308 # Normalize the config
309 config = normalize_config(config=config)
311 # Apply the config from the config file
312 for api in constants.SUPPORTED_APIS:
313 if "apis" in file_config and api in file_config["apis"].keys():
314 config["apis"][api].update(file_config["apis"][api])
315 for option in constants.OPTIONS_SET:
316 if option in file_config.keys():
317 config[option] = file_config[option]
319 # Apply the config from environment variables
320 config.update(env_config)
322 # Apply the config from the arguments
323 for api in constants.SUPPORTED_APIS:
324 if "apis" in args_config and api in args_config["apis"].keys():
325 config["apis"][api].update(args_config["apis"][api])
326 for option in constants.OPTIONS_SET:
327 if option in args_config.keys():
328 config[option] = args_config[option]
330 # Perform validation of the non-api configs
331 for key, value in config.items():
332 if key == "apis":
333 continue
335 if not is_valid_attribute(key=key, value=value):
336 sys.exit(1)
338 return config