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

3A python module to submit artifacts to an app in Veracode via their XML Upload API 

4""" 

5 

6# built-ins 

7import logging 

8from pathlib import Path 

9from typing import List, Union 

10 

11# third party 

12from requests.exceptions import HTTPError, Timeout, RequestException, TooManyRedirects 

13 

14# custom 

15from veracode.api import UploadAPI, SandboxAPI 

16from veracode.utils import validate, element_contains_error 

17from veracode import constants 

18from veracode import __project_name__ 

19 

20LOG = logging.getLogger(__project_name__ + "." + __name__) 

21 

22 

23@validate 

24def create_build(*, upload_api: UploadAPI) -> bool: 

25 """ 

26 https://help.veracode.com/reader/LMv_dtSHyb7iIxAQznC~9w/vhuQ5lMdxRNQWUK1br1mDg 

27 """ 

28 try: 

29 endpoint = "createbuild.do" 

30 params = {"app_id": upload_api.app_id, "version": upload_api.build_id} 

31 

32 # If a sandbox_id is specified, add it to the params 

33 if isinstance(upload_api.sandbox_id, str): 

34 params["sandbox_id"] = upload_api.sandbox_id 

35 

36 # Create the build 

37 response = upload_api.http_post(endpoint=endpoint, params=params) 

38 if element_contains_error(parsed_xml=response): 

39 LOG.error("Veracode returned an error when attempting to call %s", endpoint) 

40 return False 

41 return True 

42 except ( 

43 HTTPError, 

44 ConnectionError, 

45 Timeout, 

46 TooManyRedirects, 

47 RequestException, 

48 ): 

49 LOG.error("Exception encountered when calling the Veracode API") 

50 return False 

51 

52 

53@validate 

54def begin_prescan(*, upload_api: UploadAPI) -> bool: 

55 """ 

56 https://help.veracode.com/reader/LMv_dtSHyb7iIxAQznC~9w/PX5ReM5acqjM~IOVEg2~rA 

57 """ 

58 endpoint = "beginprescan.do" 

59 params = { 

60 "app_id": upload_api.app_id, 

61 "scan_all_nonfatal_top_level_modules": upload_api.scan_all_nonfatal_top_level_modules, 

62 "auto_scan": upload_api.auto_scan, 

63 } 

64 

65 # If a sandbox_id is specified, add it to the params 

66 if isinstance(upload_api.sandbox_id, str): 

67 params["sandbox_id"] = upload_api.sandbox_id 

68 

69 try: 

70 response = upload_api.http_post(endpoint=endpoint, params=params) 

71 if element_contains_error(parsed_xml=response): 

72 LOG.error("Veracode returned an error when attempting to call %s", endpoint) 

73 return False 

74 return True 

75 except ( 

76 HTTPError, 

77 ConnectionError, 

78 Timeout, 

79 TooManyRedirects, 

80 RequestException, 

81 ): 

82 LOG.error("Exception encountered when calling the Veracode API") 

83 return False 

84 

85 

86def filter_file(*, artifact: Path) -> bool: 

87 """ 

88 Filter file upload artifacts 

89 https://help.veracode.com/reader/4EKhlLSMHm5jC8P8j3XccQ/YP2ecJQmr9vE7~AkYNZJlg 

90 

91 Would prefer an API to call to pull the Veracode supported suffix(es) 

92 dynamically, but that doesn't seem to exist 

93 """ 

94 allowed = False 

95 

96 if artifact.suffix in constants.WHITELIST_FILE_SUFFIX_SET: 

97 LOG.debug("Suffix for %s is in the whitelist", artifact) 

98 allowed = True 

99 

100 if artifact.suffixes[-2:] == constants.WHITELIST_FILE_SUFFIXES_LIST: 

101 LOG.debug("Suffixes for %s are in the whitelist", artifact) 

102 allowed = True 

103 

104 if not allowed: 

105 LOG.warning( 

106 "%s was filtered out from being uploaded based on its file extension", 

107 artifact, 

108 ) 

109 

110 return allowed 

111 

112 

113@validate 

114def upload_large_file(*, upload_api: UploadAPI, artifact: Path) -> bool: 

115 """ 

116 https://help.veracode.com/reader/LMv_dtSHyb7iIxAQznC~9w/lzZ1eON0Bkr8iYjNVD9tqw 

117 

118 This API call will create a new SAST build for an existing app if one does 

119 not already exist. However, it prefers to manage the build versions so it 

120 can be mapped to code across separate systems 

121 """ 

122 filename = artifact.name 

123 

124 endpoint = "uploadlargefile.do" 

125 params = {"app_id": upload_api.app_id, "filename": filename} 

126 headers = {"Content-Type": "binary/octet-stream"} 

127 

128 # If a sandbox_id is specified, add it to the params 

129 if isinstance(upload_api.sandbox_id, str): 

130 params["sandbox_id"] = upload_api.sandbox_id 

131 

132 try: 

133 with open(artifact, "rb") as f: 

134 data = f.read() 

135 upload_api.http_post( 

136 endpoint=endpoint, 

137 data=data, 

138 params=params, 

139 headers=headers, 

140 ) 

141 return True 

142 except: 

143 LOG.error( 

144 "Error encountered when attempting to upload %s to the Veracode Upload API", 

145 filename, 

146 ) 

147 raise 

148 

149 

150@validate 

151def get_sandbox_id(*, sandbox_api: SandboxAPI) -> Union[str, None]: 

152 """ 

153 Query for and return the sandbox_id 

154 

155 https://help.veracode.com/reader/LMv_dtSHyb7iIxAQznC~9w/twPT73YBy_iQvrsGEZamhQ 

156 """ 

157 try: 

158 endpoint = "getsandboxlist.do" 

159 params = {"app_id": sandbox_api.app_id} 

160 

161 sandboxes = sandbox_api.http_get(endpoint=endpoint, params=params) 

162 

163 if element_contains_error(parsed_xml=sandboxes): 

164 LOG.error("Veracode returned an error when attempting to call %s", endpoint) 

165 raise RuntimeError 

166 

167 for sandbox in sandboxes: 

168 if sandbox_api.sandbox_name == sandbox.get("sandbox_name"): 

169 # Returns the first sandbox_name match as duplicates are not 

170 # allowed by Veracode 

171 return sandbox.get("sandbox_id") 

172 

173 # No sandbox_id exists with the provided sandbox_name 

174 LOG.info( 

175 "A sandbox named %s does not exist in application id %s", 

176 sandbox_api.sandbox_name, 

177 sandbox_api.app_id, 

178 ) 

179 return None 

180 except ( 

181 HTTPError, 

182 ConnectionError, 

183 Timeout, 

184 TooManyRedirects, 

185 RequestException, 

186 RuntimeError, 

187 ) as e: 

188 raise RuntimeError from e 

189 

190 

191@validate 

192def create_sandbox(*, sandbox_api: SandboxAPI) -> str: 

193 """ 

194 Create a sandbox and return the sandbox_id 

195 

196 https://help.veracode.com/reader/LMv_dtSHyb7iIxAQznC~9w/jp8rPey8I5WsuWz7bY2SZg 

197 """ 

198 try: 

199 endpoint = "createsandbox.do" 

200 params = { 

201 "app_id": sandbox_api.app_id, 

202 "sandbox_name": sandbox_api.sandbox_name, 

203 } 

204 

205 response = sandbox_api.http_post(endpoint=endpoint, params=params) 

206 

207 if element_contains_error(parsed_xml=response): 

208 LOG.error("Veracode returned an error when attempting to call %s", endpoint) 

209 raise RuntimeError 

210 

211 try: 

212 # Because we only make one sandbox at a time, we can use index 0 to 

213 # extract and then return the sandbox_id 

214 return response[0].get("sandbox_id") 

215 except (KeyError, IndexError) as e: 

216 LOG.error("Unable to extract the sandbox_id from the Veracode response") 

217 raise RuntimeError from e 

218 except ( 

219 HTTPError, 

220 ConnectionError, 

221 Timeout, 

222 TooManyRedirects, 

223 RequestException, 

224 RuntimeError, 

225 ) as e: 

226 raise RuntimeError from e 

227 

228 

229@validate 

230def cancel_build(*, upload_api: UploadAPI) -> bool: 

231 """ 

232 Cancel an application build 

233 

234 https://help.veracode.com/reader/LMv_dtSHyb7iIxAQznC~9w/rERUQewXKGx2D_zaoi6wGw 

235 """ 

236 try: 

237 endpoint = "deletebuild.do" 

238 params = {"app_id": upload_api.app_id} 

239 

240 # If a sandbox_id is specified, add it to the params 

241 if isinstance(upload_api.sandbox_id, str): 

242 params["sandbox_id"] = upload_api.sandbox_id 

243 

244 # https://help.veracode.com/reader/LMv_dtSHyb7iIxAQznC~9w/rERUQewXKGx2D_zaoi6wGw 

245 response = upload_api.http_get(endpoint=endpoint, params=params) 

246 

247 if element_contains_error(parsed_xml=response): 

248 LOG.error("Veracode returned an error when attempting to call %s", endpoint) 

249 return False 

250 

251 if isinstance(upload_api.sandbox_id, str): 

252 LOG.info( 

253 "Successfully cancelled the build in sandbox id %s of application id %s", 

254 upload_api.sandbox_id, 

255 upload_api.app_id, 

256 ) 

257 return True 

258 

259 LOG.info( 

260 "Successfully cancelled the build application id %s", upload_api.app_id 

261 ) 

262 return True 

263 except ( 

264 HTTPError, 

265 ConnectionError, 

266 Timeout, 

267 TooManyRedirects, 

268 RequestException, 

269 RuntimeError, 

270 ): 

271 return False 

272 

273 

274@validate 

275def setup_scan_prereqs(*, upload_api: UploadAPI) -> bool: 

276 """ 

277 Setup the scan environment prereqs 

278 """ 

279 

280 if not build_exists(upload_api=upload_api): 

281 LOG.info( 

282 "app_id %s was not ready for a new build", 

283 upload_api.app_id, 

284 ) 

285 

286 if not cancel_build(upload_api=upload_api): 

287 LOG.debug("Unable to cancel build for app_id %s", upload_api.app_id) 

288 return False 

289 

290 if create_build(upload_api=upload_api): 

291 LOG.info( 

292 "Successfully created an application build for app id %s", 

293 upload_api.app_id, 

294 ) 

295 return True 

296 

297 LOG.error("Failed to create an application build for app id %s", upload_api.app_id) 

298 

299 return False 

300 

301 

302@validate 

303def submit_artifacts( # pylint: disable=too-many-return-statements, too-many-branches 

304 *, upload_api: UploadAPI, sandbox_api: SandboxAPI = None 

305) -> bool: 

306 """ 

307 Submit build artifacts to Veracode for SAST 

308 """ 

309 artifacts: List[Path] = [] 

310 

311 # If we were provided a sandbox_api object, attempt to get the sandbox_id 

312 # that maps to the sandbox_name and if it doesn't exist, make one 

313 if sandbox_api: 

314 try: 

315 upload_api.sandbox_id = get_sandbox_id(sandbox_api=sandbox_api) 

316 except RuntimeError: 

317 LOG.warning( 

318 "Unable to get the sandbox_id for sandbox_name %s", 

319 sandbox_api.sandbox_name, 

320 ) 

321 return False 

322 

323 if not upload_api.sandbox_id: 

324 try: 

325 upload_api.sandbox_id = create_sandbox(sandbox_api=sandbox_api) 

326 except RuntimeError: 

327 LOG.error( 

328 "Unable to create a sandbox named %s in app_id %s", 

329 sandbox_api.sandbox_name, 

330 sandbox_api.app_id, 

331 ) 

332 return False 

333 

334 # Setup the scan prereqs 

335 if not setup_scan_prereqs(upload_api=upload_api): 

336 # Scan prereq setup failed 

337 return False 

338 LOG.info("Successfully setup the scan prerequisites in Veracode") 

339 

340 LOG.info("Beginning pre-upload file filtering") 

341 for artifact in upload_api.build_dir.iterdir(): 

342 LOG.debug("Calling filter_file on %s", artifact) 

343 if filter_file(artifact=artifact): 

344 artifacts += [artifact] 

345 

346 # Check to see if the artifacts list is empty 

347 if not artifacts: 

348 LOG.error("Nothing to upload") 

349 return False 

350 

351 LOG.info("Beginning file uploads") 

352 for artifact in artifacts: 

353 if upload_large_file(upload_api=upload_api, artifact=artifact): 

354 LOG.info("Successfully uploaded %s", artifact) 

355 else: 

356 LOG.error("Failed to upload %s", artifact) 

357 return False 

358 

359 LOG.info("File uploads complete") 

360 if begin_prescan(upload_api=upload_api): 

361 LOG.info("Successfully began the prescan") 

362 else: 

363 LOG.error("Failed to start the prescan") 

364 return False 

365 

366 return True 

367 

368 

369@validate 

370def build_exists(*, upload_api: UploadAPI) -> bool: 

371 """ 

372 Return whether a build already exists 

373 

374 https://help.veracode.com/reader/orRWez4I0tnZNaA_i0zn9g/Yjclv0XIfU1v_yqmkt18zA 

375 """ 

376 

377 try: 

378 endpoint = "getbuildlist.do" 

379 params = {"app_id": upload_api.app_id} 

380 

381 # If a sandbox_id is specified, add it to the params 

382 if isinstance(upload_api.sandbox_id, str): 

383 params["sandbox_id"] = upload_api.sandbox_id 

384 

385 response = upload_api.http_get(endpoint=endpoint, params=params) 

386 

387 if element_contains_error(parsed_xml=response): 

388 LOG.error("Veracode returned an error when attempting to call %s", endpoint) 

389 raise RuntimeError 

390 

391 try: 

392 # If we don't throw, at least one build already exists 

393 response[0].get("build_id") 

394 return False 

395 

396 except IndexError: 

397 # No existing builds 

398 return True 

399 

400 except ( 

401 HTTPError, 

402 ConnectionError, 

403 Timeout, 

404 TooManyRedirects, 

405 RequestException, 

406 RuntimeError, 

407 ) as e: 

408 raise RuntimeError from e