Coverage for veracode/submit_artifacts.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"""
3A python module to submit artifacts to an app in Veracode via their XML Upload API
4"""
6# built-ins
7import logging
8from pathlib import Path
9from typing import List, Union
11# third party
12from requests.exceptions import HTTPError, Timeout, RequestException, TooManyRedirects
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__
20LOG = logging.getLogger(__project_name__ + "." + __name__)
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}
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
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
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 }
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
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
86def filter_file(*, artifact: Path) -> bool:
87 """
88 Filter file upload artifacts
89 https://help.veracode.com/reader/4EKhlLSMHm5jC8P8j3XccQ/YP2ecJQmr9vE7~AkYNZJlg
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
96 if artifact.suffix in constants.WHITELIST_FILE_SUFFIX_SET:
97 LOG.debug("Suffix for %s is in the whitelist", artifact)
98 allowed = True
100 if artifact.suffixes[-2:] == constants.WHITELIST_FILE_SUFFIXES_LIST:
101 LOG.debug("Suffixes for %s are in the whitelist", artifact)
102 allowed = True
104 if not allowed:
105 LOG.warning(
106 "%s was filtered out from being uploaded based on its file extension",
107 artifact,
108 )
110 return allowed
113@validate
114def upload_large_file(*, upload_api: UploadAPI, artifact: Path) -> bool:
115 """
116 https://help.veracode.com/reader/LMv_dtSHyb7iIxAQznC~9w/lzZ1eON0Bkr8iYjNVD9tqw
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
124 endpoint = "uploadlargefile.do"
125 params = {"app_id": upload_api.app_id, "filename": filename}
126 headers = {"Content-Type": "binary/octet-stream"}
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
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
150@validate
151def get_sandbox_id(*, sandbox_api: SandboxAPI) -> Union[str, None]:
152 """
153 Query for and return the sandbox_id
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}
161 sandboxes = sandbox_api.http_get(endpoint=endpoint, params=params)
163 if element_contains_error(parsed_xml=sandboxes):
164 LOG.error("Veracode returned an error when attempting to call %s", endpoint)
165 raise RuntimeError
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")
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
191@validate
192def create_sandbox(*, sandbox_api: SandboxAPI) -> str:
193 """
194 Create a sandbox and return the sandbox_id
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 }
205 response = sandbox_api.http_post(endpoint=endpoint, params=params)
207 if element_contains_error(parsed_xml=response):
208 LOG.error("Veracode returned an error when attempting to call %s", endpoint)
209 raise RuntimeError
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
229@validate
230def cancel_build(*, upload_api: UploadAPI) -> bool:
231 """
232 Cancel an application build
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}
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
244 # https://help.veracode.com/reader/LMv_dtSHyb7iIxAQznC~9w/rERUQewXKGx2D_zaoi6wGw
245 response = upload_api.http_get(endpoint=endpoint, params=params)
247 if element_contains_error(parsed_xml=response):
248 LOG.error("Veracode returned an error when attempting to call %s", endpoint)
249 return False
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
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
274@validate
275def setup_scan_prereqs(*, upload_api: UploadAPI) -> bool:
276 """
277 Setup the scan environment prereqs
278 """
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 )
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
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
297 LOG.error("Failed to create an application build for app id %s", upload_api.app_id)
299 return False
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] = []
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
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
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")
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]
346 # Check to see if the artifacts list is empty
347 if not artifacts:
348 LOG.error("Nothing to upload")
349 return False
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
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
366 return True
369@validate
370def build_exists(*, upload_api: UploadAPI) -> bool:
371 """
372 Return whether a build already exists
374 https://help.veracode.com/reader/orRWez4I0tnZNaA_i0zn9g/Yjclv0XIfU1v_yqmkt18zA
375 """
377 try:
378 endpoint = "getbuildlist.do"
379 params = {"app_id": upload_api.app_id}
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
385 response = upload_api.http_get(endpoint=endpoint, params=params)
387 if element_contains_error(parsed_xml=response):
388 LOG.error("Veracode returned an error when attempting to call %s", endpoint)
389 raise RuntimeError
391 try:
392 # If we don't throw, at least one build already exists
393 response[0].get("build_id")
394 return False
396 except IndexError:
397 # No existing builds
398 return True
400 except (
401 HTTPError,
402 ConnectionError,
403 Timeout,
404 TooManyRedirects,
405 RequestException,
406 RuntimeError,
407 ) as e:
408 raise RuntimeError from e