defectdojo-cli2 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ from .util import Util
2
+ from .findings import Findings
3
+ from .engagements import Engagements
4
+ from .tests import Tests
5
+ import pkg_resources # part of setuptools
6
+
7
+ __version__ = pkg_resources.get_distribution("defectdojo_cli2").version
@@ -0,0 +1,45 @@
1
+ import sys
2
+ import argparse
3
+ from defectdojo_cli2 import Findings
4
+ from defectdojo_cli2 import Engagements
5
+ from defectdojo_cli2 import Tests
6
+ from defectdojo_cli2 import __version__
7
+
8
+ # Multilevel argparse based on https://chase-seibert.github.io/blog/2014/03/21/python-multilevel-argparse.html
9
+ class DefectDojoCLI(object):
10
+ def parse_cli_args(self):
11
+ parser = argparse.ArgumentParser(
12
+ description='CLI wrapper for DefectDojo using APIv2',
13
+ usage='''defectdojo <command> [<args>]
14
+
15
+ You can use the following commands:
16
+ findings Operations related to findings (findings --help for more details)
17
+ engagements Operations related to engagements (engagements --help for more details)
18
+ tests Operations related to tests (tests --help for more details)
19
+ ''')
20
+ parser.add_argument('command', help='Command to run')
21
+ parser.add_argument('-v', '--version', action='version', version='%(prog)s_cli v' + __version__)
22
+ # Parse_args defaults to [1:] for args, but you need to
23
+ # exclude the rest of the args too, or validation will fail
24
+ args = parser.parse_args(sys.argv[1:2])
25
+ if not hasattr(self, '_'+args.command):
26
+ print('Unrecognized command')
27
+ parser.print_help()
28
+ exit(1)
29
+ # Use dispatch pattern to invoke method with same name (that starts with _)
30
+ getattr(self, '_'+args.command)()
31
+
32
+ def _findings(self):
33
+ Findings().parse_cli_args()
34
+
35
+ def _engagements(self):
36
+ Engagements().parse_cli_args()
37
+
38
+ def _tests(self):
39
+ Tests().parse_cli_args()
40
+
41
+ def main():
42
+ DefectDojoCLI().parse_cli_args()
43
+
44
+ if __name__ == '__main__':
45
+ main()
@@ -0,0 +1,373 @@
1
+ from datetime import datetime
2
+ import json
3
+ import sys
4
+ import argparse
5
+ import requests
6
+ from unittest.mock import PropertyMock
7
+ from defectdojo_cli2.util import Util
8
+ from defectdojo_cli2.tests import Tests
9
+
10
+ class Engagements(object):
11
+ def parse_cli_args(self):
12
+ parser = argparse.ArgumentParser(
13
+ description='Perform <sub_command> related to engagements on DefectDojo',
14
+ usage='''defectdojo engagements <sub_command> [<args>]
15
+
16
+ You can use the following sub_commands:
17
+ create Create an engagement (engagements create --help for more details)
18
+ close Close an engagement (engagements close --help for more details)
19
+ update Update an engagement (engagements update --help for more details)
20
+ ''')
21
+ parser.add_argument('sub_command', help='Sub_command to run')
22
+ # Get sub_command
23
+ args = parser.parse_args(sys.argv[2:3])
24
+ if not hasattr(self, '_'+args.sub_command):
25
+ print('Unrecognized sub_command')
26
+ parser.print_help()
27
+ exit(1)
28
+ # Use dispatch pattern to invoke method with same name (that starts with _)
29
+ getattr(self, '_'+args.sub_command)()
30
+
31
+ def create(self, url, api_key, name, desc, product_id, lead_id,
32
+ start_date=None, end_date=None, engagement_type=None,
33
+ status=None, build_id=None, repo_url=None, branch_tag=None,
34
+ commit_hash=None, product_version=None, tracker=None,
35
+ tag=None, local_dedup=None, **kwargs):
36
+ # Prepare JSON data to be send
37
+ request_json = dict()
38
+ API_URL = url+'/api/v2'
39
+ ENGAGEMENTS_URL = API_URL+'/engagements/'
40
+ request_json['name'] = name
41
+ request_json['description'] = desc
42
+ request_json['product'] = product_id
43
+ request_json['lead'] = lead_id
44
+ if start_date is not None:
45
+ request_json['target_start'] = start_date
46
+ if end_date is not None:
47
+ request_json['target_end'] = end_date
48
+ if engagement_type is not None:
49
+ request_json['engagement_type'] = engagement_type
50
+ if status is not None:
51
+ request_json['status'] = status
52
+ if build_id is not None:
53
+ request_json['build_id'] = build_id
54
+ if repo_url is not None:
55
+ request_json['source_code_management_uri'] = repo_url
56
+ if branch_tag is not None:
57
+ request_json['branch_tag'] = branch_tag
58
+ if commit_hash is not None:
59
+ request_json['commit_hash'] = commit_hash
60
+ if product_version is not None:
61
+ request_json['version'] = product_version
62
+ if tracker is not None:
63
+ request_json['tracker'] = tracker
64
+ if tag is not None:
65
+ request_json['tags'] = tag
66
+ if local_dedup is not None:
67
+ if local_dedup == 'true':
68
+ request_json['deduplication_on_engagement'] = True
69
+ else:
70
+ request_json['deduplication_on_engagement'] = False
71
+ request_json = json.dumps(request_json)
72
+
73
+ # Make the request
74
+ response = Util().request_apiv2('POST', ENGAGEMENTS_URL, api_key, data=request_json)
75
+ return response
76
+
77
+ def _create(self):
78
+ # Read user-supplied arguments
79
+ parser = argparse.ArgumentParser(description='Create an engagement on DefectDojo',
80
+ usage='defectdojo engagements create [<args>]')
81
+ optional = parser._action_groups.pop()
82
+ required = parser.add_argument_group('required arguments')
83
+ required.add_argument('--url',
84
+ help='DefectDojo URL', required=True)
85
+ required.add_argument('--api_key',
86
+ help='API v2 Key', required=True)
87
+ required.add_argument('--name',
88
+ help='Engagement name', required=True)
89
+ required.add_argument('--desc',
90
+ help='Engagement description',
91
+ required=True, metavar='DESCRIPTION')
92
+ required.add_argument('--product_id',
93
+ help='ID of the product on which the engagement will be created',
94
+ required=True)
95
+ required.add_argument('--lead_id',
96
+ help='ID of the user responsible for this engagement',
97
+ required=True)
98
+ optional.add_argument('--start_date',
99
+ help='Engagement starting date (default=TODAY)',
100
+ metavar='YYYY-MM-DD', default=datetime.now().strftime('%Y-%m-%d'))
101
+ optional.add_argument('--end_date',
102
+ help='Engagement ending date (default=TODAY)',
103
+ metavar='YYYY-MM-DD', default=datetime.now().strftime('%Y-%m-%d'))
104
+ optional.add_argument('--type',
105
+ help='Engagement type (default = "CI/CD")',
106
+ choices=['Interactive', 'CI/CD'], default='CI/CD')
107
+ optional.add_argument('--status',
108
+ help='Engagement status (default = "In Progress")',
109
+ choices=['Not Started', 'Blocked', 'Cancelled',
110
+ 'Completed', 'In Progress', 'On Hold',
111
+ 'Waiting for Resource'],
112
+ default='In Progress')
113
+ optional.add_argument('--build_id',
114
+ help='Build ID')
115
+ optional.add_argument('--repo_url',
116
+ help='Link to source code management')
117
+ optional.add_argument('--branch_tag',
118
+ help='Tag or branch of the product the engagement tested',
119
+ metavar='TAG_OR_BRANCH')
120
+ optional.add_argument('--commit_hash',
121
+ help='Commit HASH')
122
+ optional.add_argument('--product_version',
123
+ help='Version of the product the engagement tested')
124
+ optional.add_argument('--tracker',
125
+ help='Link to epic or ticket system with changes to version.')
126
+ optional.add_argument(
127
+ '--tag',
128
+ help='Engagement tag (can be used multiple times)',
129
+ action='append'
130
+ )
131
+ optional.add_argument(
132
+ '--local_dedup',
133
+ help='If enabled deduplication will only mark a finding in '
134
+ 'this engagement as duplicate of another finding if both '
135
+ 'findings are in this engagement. If disabled, deduplication '
136
+ 'is on the product level. (default = false)',
137
+ choices=['true', 'false'],
138
+ default='false'
139
+ )
140
+ parser._action_groups.append(optional)
141
+ # Parse out arguments ignoring the first three (because we're inside a sub_command)
142
+ args = vars(parser.parse_args(sys.argv[3:]))
143
+
144
+ # Adjust args
145
+ if args['type'] is not None:
146
+ # Rename key from 'type' to 'engagement_type' to match the argument of self.create
147
+ args['engagement_type'] = args.pop('type')
148
+
149
+ # Create engagement
150
+ response = self.create(**args)
151
+
152
+ # Pretty print JSON response
153
+ Util().default_output(response, sucess_status_code=201)
154
+
155
+ def close(self, url, api_key, engagement_id, **kwargs):
156
+ # Prepare parameters
157
+ API_URL = url+'/api/v2'
158
+ ENGAGEMENTS_URL = API_URL+'/engagements/'
159
+ ENGAGEMENTS_ID_URL = ENGAGEMENTS_URL+engagement_id
160
+ ENGAGEMENTS_CLOSE_URL = ENGAGEMENTS_ID_URL+'/close/'
161
+ # Make the request
162
+ response = Util().request_apiv2('POST', ENGAGEMENTS_CLOSE_URL, api_key)
163
+ return response
164
+
165
+ def _close(self):
166
+ # Read user-supplied arguments
167
+ parser = argparse.ArgumentParser(description='Close an engagement on DefectDojo',
168
+ usage='defectdojo engagements close ENGAGEMENT_ID')
169
+ required = parser.add_argument_group('required arguments')
170
+ parser.add_argument('engagement_id', help='ID of the engagement to be closed')
171
+ required.add_argument('--url', help='DefectDojo URL', required=True)
172
+ required.add_argument('--api_key', help='API v2 Key', required=True)
173
+ # Parse out arguments ignoring the first three (because we're inside a sub_command)
174
+ args = vars(parser.parse_args(sys.argv[3:]))
175
+
176
+ # Close engagement
177
+ response = self.close(**args)
178
+
179
+ # DefectDojo doesnt has an output when a engagement is successfully closed so we need to create one
180
+ if response.status_code == 200:
181
+ type(response).text = PropertyMock(return_value='{"return": "sucess"}')
182
+ # Pretty print JSON response
183
+ Util().default_output(response, sucess_status_code=200)
184
+
185
+ def update(self, url, api_key, engagement_id, name=None, desc=None, product_id=None, lead_id=None,
186
+ start_date=None, end_date=None, engagement_type=None, repo_url=None, branch_tag=None,
187
+ product_version=None, status=None, **kwargs):
188
+ # Prepare JSON data to be send
189
+ request_json = dict()
190
+ API_URL = url+'/api/v2'
191
+ ENGAGEMENTS_URL = API_URL+'/engagements/'
192
+ ENGAGEMENTS_ID_URL = ENGAGEMENTS_URL+engagement_id+'/'
193
+ if name is not None:
194
+ request_json['name'] = name
195
+ if desc is not None:
196
+ request_json['description'] = desc
197
+ if product_id is not None:
198
+ request_json['product'] = product_id
199
+ if lead_id is not None:
200
+ request_json['lead'] = lead_id
201
+ if start_date is not None:
202
+ request_json['target_start'] = start_date
203
+ if end_date is not None:
204
+ request_json['target_end'] = end_date
205
+ if engagement_type is not None:
206
+ request_json['engagement_type'] = engagement_type
207
+ if repo_url is not None:
208
+ request_json['source_code_management_uri'] = repo_url
209
+ if branch_tag is not None:
210
+ request_json['branch_tag'] = branch_tag
211
+ if product_version is not None:
212
+ request_json['version'] = product_version
213
+ if status is not None:
214
+ request_json['status'] = status
215
+ request_json = json.dumps(request_json)
216
+
217
+ # Make the request
218
+ response = Util().request_apiv2('PATCH', ENGAGEMENTS_ID_URL, api_key, data=request_json)
219
+ return response
220
+
221
+ def _update(self):
222
+ # Read user-supplied arguments
223
+ parser = argparse.ArgumentParser(description='Update a engagement on DefectDojo',
224
+ usage='defectdojo engagements update ENGAGEMENT_ID [<args>]')
225
+ optional = parser._action_groups.pop()
226
+ required = parser.add_argument_group('required arguments')
227
+ parser.add_argument('engagement_id', help='ID of the engagement to be updated')
228
+ required.add_argument('--url', help='DefectDojo URL', required=True)
229
+ required.add_argument('--api_key', help='API v2 Key', required=True)
230
+ optional.add_argument('--name', help='Engagement name')
231
+ optional.add_argument('--desc', help='Engagement description', metavar='DESCRIPTION')
232
+ optional.add_argument('--product_id', help='ID of the product the engagement belongs to')
233
+ optional.add_argument('--lead_id', help='ID of the user responsible for this engagement')
234
+ optional.add_argument('--start_date', help='Engagement starting date', metavar='YYYY-MM-DD')
235
+ optional.add_argument('--end_date', help='Engagement ending date', metavar='YYYY-MM-DD')
236
+ optional.add_argument('--type', help='Engagement type', choices=['Interactive', 'CI/CD'])
237
+ optional.add_argument('--repo_url', help='Link to source code')
238
+ optional.add_argument('--branch_tag', help='Tag or branch of the product the engagement tested',
239
+ metavar='TAG_OR_BRANCH')
240
+ optional.add_argument('--product_version', help='Version of the product the engagement tested')
241
+ optional.add_argument('--status', help='Engagement status',
242
+ choices=['Not Started', 'Blocked', 'Cancelled', 'Completed', 'In Progress',
243
+ 'On Hold', 'Waiting for Resource'])
244
+ parser._action_groups.append(optional)
245
+ # Parse out arguments ignoring the first three (because we're inside a sub_command)
246
+ args = vars(parser.parse_args(sys.argv[3:]))
247
+
248
+ # Update engagement
249
+ response = self.update(**args)
250
+
251
+ # Pretty print JSON response
252
+ Util().default_output(response, sucess_status_code=200)
253
+
254
+ def list(self, url, api_key, name=None, product_id=None, **kwargs):
255
+ # Create parameters to be requested
256
+ request_params = dict()
257
+ API_URL = url+'/api/v2'
258
+ ENGAGEMENTS_URL = API_URL+'/engagements/'
259
+ if name is not None:
260
+ request_params['name'] = name
261
+ if product_id is not None:
262
+ request_params['product'] = product_id
263
+
264
+ # Make the request
265
+ response = Util().request_apiv2('GET', ENGAGEMENTS_URL, api_key, params=request_params)
266
+ return response
267
+
268
+ def _list(self):
269
+ # Read user-supplied arguments
270
+ parser = argparse.ArgumentParser(description='List an engagement on DefectDojo',
271
+ usage='defectdojo engagements list [<args>]')
272
+ optional = parser._action_groups.pop()
273
+ required = parser.add_argument_group('required arguments')
274
+ required.add_argument(
275
+ '--url',
276
+ help='DefectDojo URL',
277
+ required=True
278
+ )
279
+ required.add_argument(
280
+ '--api_key',
281
+ help='API v2 Key',
282
+ required=True
283
+ )
284
+ optional.add_argument(
285
+ '--name',
286
+ help='Engagement name'
287
+ )
288
+ optional.add_argument(
289
+ '--product_id',
290
+ help='Product ID'
291
+ )
292
+ parser._action_groups.append(optional)
293
+ # Parse out arguments ignoring the first three (because we're inside a sub_command)
294
+ args = vars(parser.parse_args(sys.argv[3:]))
295
+
296
+ # Update engagement
297
+ response = self.list(**args)
298
+
299
+ # Pretty print JSON response
300
+ Util().default_output(response, sucess_status_code=200)
301
+
302
+ def reopen(self, url, api_key, engagement_id, **kwargs):
303
+ # Prepare parameters
304
+ API_URL = url+'/api/v2'
305
+ ENGAGEMENTS_URL = API_URL+'/engagements/'
306
+ ENGAGEMENTS_ID_URL = ENGAGEMENTS_URL+engagement_id
307
+ ENGAGEMENTS_CLOSE_URL = ENGAGEMENTS_ID_URL+'/reopen/'
308
+ # Make the request
309
+ response = Util().request_apiv2('POST', ENGAGEMENTS_CLOSE_URL, api_key)
310
+ return response
311
+
312
+ def _reopen(self):
313
+ # Read user-supplied arguments
314
+ parser = argparse.ArgumentParser(description='Reopen an engagement on DefectDojo',
315
+ usage='defectdojo engagements reopen ENGAGEMENT_ID')
316
+ required = parser.add_argument_group('required arguments')
317
+ parser.add_argument('engagement_id', help='ID of the engagement to be reopened')
318
+ required.add_argument('--url', help='DefectDojo URL', required=True)
319
+ required.add_argument('--api_key', help='API v2 Key', required=True)
320
+ # Parse out arguments ignoring the first three (because we're inside a sub_command)
321
+ args = vars(parser.parse_args(sys.argv[3:]))
322
+
323
+ # Close engagement
324
+ response = self.reopen(**args)
325
+
326
+ # DefectDojo doesnt has an output when a engagement is successfully reopened so we need to create one
327
+ if response.status_code == 200:
328
+ type(response).text = PropertyMock(return_value='{"return": "sucess"}')
329
+ # Pretty print JSON response
330
+ Util().default_output(response, sucess_status_code=200)
331
+
332
+ def get_engagements_by_test_tags(self, url, api_key, tags, tags_operator):
333
+ request_params = dict()
334
+ request_params['url'] = url
335
+ request_params['api_key'] = api_key
336
+
337
+ if tags_operator == 'union': # Default behaviour
338
+ # Make a request to API to list all tests with the tags we're looking for
339
+ request_params['tag'] = tags
340
+ response = Tests().list(**request_params)
341
+ # Parse output
342
+ json_out = json.loads(response.text)
343
+ results = json_out['results']
344
+ # Create set of all engagements from the response
345
+ engagement_set = set()
346
+ for test in results:
347
+ engagement_set.add(str(test['engagement']))
348
+ # Transform set to list
349
+ engagement_list = list(engagement_set)
350
+
351
+ elif tags_operator == 'intersect':
352
+ engagement_list_of_sets = list()
353
+ for tag in tags:
354
+ # Make a request to API to list all tests with the tags we're looking for
355
+ request_params['tag'] = tag
356
+ response = Tests().list(**request_params)
357
+ # Parse output
358
+ json_out = json.loads(response.text)
359
+ results = json_out['results']
360
+ # Create set of all engagements from the response
361
+ engagement_set = set()
362
+ for test in results:
363
+ engagement_set.add(str(test['engagement']))
364
+ # Add set of engagement to list
365
+ engagement_list_of_sets.append(engagement_set)
366
+
367
+ # Get intersection between all sets
368
+ engagement_intersection = engagement_list_of_sets[0]
369
+ for engagement_set in engagement_list_of_sets[1:]:
370
+ engagement_intersection.intersection_update(engagement_set)
371
+ engagement_list = engagement_intersection
372
+
373
+ return list(engagement_list)