Hide keyboard shortcuts

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""" 

5 

6# built-ins 

7from argparse import ArgumentParser 

8import logging 

9from pathlib import Path 

10from typing import Any, Dict, List, Union 

11import os 

12import sys 

13 

14# third party 

15import yaml 

16 

17# custom 

18from veracode.api import is_valid_attribute 

19from veracode import constants 

20from veracode import __version__ 

21 

22LOG = logging.getLogger(__name__) 

23 

24 

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 

38 

39 

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 

53 

54 

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) 

62 

63 # If filtering did not result in any changes, return the config 

64 if config == filtered_config: 

65 return config 

66 

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 

74 

75 

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' 

85 

86 return default_config 

87 

88 

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 

101 

102 return normalized_file_config 

103 

104 

105def parse_file_config(*, config_file: Path) -> Dict: 

106 """ 

107 Parse the sast-veracode config file 

108 """ 

109 # Filter 

110 suffix_whitelist = {".yml", ".yaml"} 

111 

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 {} 

115 

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 

132 

133 return config 

134 

135 

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 

144 

145 

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"] = {} 

154 

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] = {} 

159 

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 

168 

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 ] 

175 

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] 

180 

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] 

188 

189 # Filter the config 

190 config = filter_config(config=config) 

191 

192 return config 

193 

194 

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()) 

201 

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 } 

217 

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] 

223 

224 for key, value in disable_keys.items(): 

225 if value in parsed_args.keys(): 

226 args_config[key] = not parsed_args[value] 

227 

228 normalized_args_config = normalize_config(config=args_config) 

229 

230 return normalized_args_config 

231 

232 

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 ) 

276 

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 

293 

294 

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() 

303 

304 ## Create the final config 

305 # Start with the (naive) default 

306 config = default_config 

307 

308 # Normalize the config 

309 config = normalize_config(config=config) 

310 

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] 

318 

319 # Apply the config from environment variables 

320 config.update(env_config) 

321 

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] 

329 

330 # Perform validation of the non-api configs 

331 for key, value in config.items(): 

332 if key == "apis": 

333 continue 

334 

335 if not is_valid_attribute(key=key, value=value): 

336 sys.exit(1) 

337 

338 return config