36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400 | class SSPMarkdownWriter():
"""
Class to write control responses as markdown.
Functions in this class are mainly used by jinja and not by the trestle code itself.
"""
def __init__(self, trestle_root: pathlib.Path) -> None:
"""Initialize the class."""
self._trestle_root = trestle_root
self._ssp: ssp.SystemSecurityPlan = None
self._resolved_catalog: Catalog = None
self._catalog_interface: CatalogInterface = None
def set_ssp(self, ssp: ssp.SystemSecurityPlan) -> None:
"""Set ssp."""
self._ssp = ssp
def set_catalog(self, resolved_catalog: Catalog) -> None:
"""Set catalog."""
self._resolved_catalog = resolved_catalog
self._catalog_interface = catalog_interface.CatalogInterface(self._resolved_catalog)
def get_control_statement(self, control_id: str, level: int) -> str:
"""
Get the control statement for an ssp - to be printed in markdown as a structured list.
Args:
control_id: The control_id to use.
Returns:
A markdown blob as a string.
"""
if not self._resolved_catalog:
raise TrestleError('Cannot get control statement, set resolved catalog first.')
writer = DocsControlWriter()
control = self._catalog_interface.get_control(control_id)
if not control:
return ''
control_lines = writer.get_control_statement_ssp(control)
return self._build_tree_and_adjust(control_lines, level)
def get_control_part(self, control_id: str, part_name: str, level: int) -> str:
"""Get control part with given name."""
control_part = self._catalog_interface.get_control_part_prose(control_id, part_name)
md_list = self._write_str_with_header(
f'Control Part: {part_name} for control: {control_id}', control_part, level
)
return self._build_tree_and_adjust(md_list.split('\n'), level)
def get_fedramp_control_tables(self, control_id: str, level: int, label_column: bool = False) -> str:
"""Get the fedramp metadata as markdown tables, with optional third label column for params.
The fedramp metadata has the following elements:
- Responsible roles field
- Parameter values table
- Implementation status field
- Control origination field
Returns:
tables as one coherent markdown blob.
"""
resp_roles_table = self.get_responsible_roles_table(control_id, level)
params_values = self._parameter_table(control_id, level, label_column)
impl_status = self.get_fedramp_implementation_status(control_id, level)
control_orig = self.get_fedramp_control_origination(control_id, level)
final_output = ''
if resp_roles_table:
final_output += resp_roles_table
if params_values:
final_output += '\n' + params_values
if impl_status:
final_output += '\n' + impl_status
if control_orig:
final_output += '\n' + control_orig
return final_output
def get_responsible_roles_table(self, control_id: str, level: int) -> str:
"""
For each role id - if the role exists in metadata use the title as what gets printed in the roles table.
If not (for now) warn and use the role-id as the printed text.
"""
if self._ssp is None:
raise TrestleError('Cannot get responsible roles, SSP is not set.')
for impl_requirement in self._ssp.control_implementation.implemented_requirements:
if impl_requirement.control_id == control_id:
if impl_requirement.responsible_roles:
resp_roles = as_list(impl_requirement.responsible_roles)
role_ids = [role.role_id.replace('_', ' ') for role in resp_roles]
# now check if this role exists in the metadata
role_titles = dict(zip(role_ids, role_ids))
roles = as_list(self._ssp.metadata.roles)
for role in roles:
if role.id in role_ids:
role_titles[role.id] = role.title
# dictionary to md table
md_list = self._write_table_with_header(
'Responsible Roles.', [[key, role_titles[key]] for key in role_titles.keys()],
['Role ID', 'Title'],
level
)
return md_list
else:
logger.warning(
f'No responsible roles were found for the control with id: {control_id} in given SSP.'
)
return ''
return ''
def _parameter_table(self, control_id: str, level: int, label_column: bool = False) -> str:
"""Print Param_id | ValueOrLabelOrChoices | Optional Label Column."""
if not self._ssp:
raise TrestleError('Cannot get parameter table, set SSP first.')
writer = DocsControlWriter()
control = self._catalog_interface.get_control(control_id)
if not control:
return ''
params_lines = writer.get_param_table(control, label_column)
# need to make sure no params still have moustaches. convert to brackets to avoid jinja complaints
clean_lines = []
for line in params_lines:
clean_lines.append(line.replace('{{', '[[').replace('}}', ']]'))
tree = DocsMarkdownNode.build_tree_from_markdown(clean_lines)
tree.change_header_level_by(level)
return tree.content.raw_text
def get_fedramp_implementation_status(self, control_id: str, level: int) -> str:
"""
Print implementation status as a list of items, only showing those that are applicable for the control.
This is unlike the word document FedRAMP which uses checkboxes on standard set of options.
Using a LUT to map between structured data fields, defined by FedRAMP and historical text.
"""
if not self._ssp:
raise TrestleError('Cannot get Fedramp implementation status, set SSP first.')
implementation_statuses: List[str] = []
control_impl_req = self._control_implemented_req(control_id)
if control_impl_req and control_impl_req.props:
for prop in control_impl_req.props:
if prop.name == IMPLEMENTATION_STATUS:
implementation_statuses.append(prop.value)
md_list = self._write_list_with_header('FedRamp Implementation Status.', implementation_statuses, level)
return md_list
def get_fedramp_control_origination(self, control_id: str, level: int) -> str:
"""
Print control origination, as a list of items, only showing those that are applicable for the control.
Using a LUT to map between structured data fields, defined by FedRAMP and historical text.
"""
if not self._ssp:
raise TrestleError('Cannot get FedRamp control origination, set SSP first.')
control_origination = []
control_impl_req = self._control_implemented_req(control_id)
if control_impl_req and control_impl_req.props:
for prop in control_impl_req.props:
if prop.name == CONTROL_ORIGINATION:
control_origination.append(prop.value)
md_list = self._write_list_with_header('FedRamp Control Origination.', control_origination, level)
return md_list
@staticmethod
def _write_component_prompt(
md_writer: MDWriter,
comp_name: str,
prose: str,
rules: List[str],
status: str,
show_rules: bool,
show_status: bool
) -> None:
header = f'Component: {comp_name}'
md_writer.new_header(1, header)
md_writer.set_indent_level(-1)
md_writer.new_line(prose)
md_writer.set_indent_level(-1)
if rules and show_rules:
md_writer.new_header(2, title='Rules:')
md_writer.set_indent_level(-1)
md_writer.new_list(rules)
md_writer.set_indent_level(-1)
if status and show_status:
md_writer.new_header(2, title=f'Implementation Status: {status}')
def get_control_response(
self,
control_id: str,
level: int,
write_empty_responses: bool = False,
show_comp: bool = True,
show_rules: bool = False,
show_status: bool = True
) -> str:
"""
Get the full control implemented requirements, broken down based on the available control responses.
Args:
control_id: id of the control
level: level of indentation
write_empty_responses: write response even if empty
show_comp: show the component name in the response
Notes:
This is intended to be invoked from a jinja template that has already written out the prompt for
control response
"""
if not self._resolved_catalog:
raise TrestleError('Cannot get control response, set resolved catalog first.')
control = self._catalog_interface.get_control(control_id)
imp_req = self._control_implemented_req(control_id)
if not imp_req:
logger.info(f'No implemented requirements found for the control {control_id}')
return ''
md_writer = MDWriter(None)
system_prose = ''
system_rules = []
system_status = STATUS_OPERATIONAL
imp_req_responses = self._get_responses_by_components(imp_req, write_empty_responses)
if SSP_MAIN_COMP_NAME in imp_req_responses:
system_prose, system_rules, system_status = imp_req_responses[SSP_MAIN_COMP_NAME]
SSPMarkdownWriter._write_component_prompt(
md_writer, SSP_MAIN_COMP_NAME, system_prose, system_rules, system_status, show_rules, show_status
)
# if a control has no statement sub-parts then get the response bycomps from the imp_req itself
# otherwise get them from the statements in the imp_req
# an imp_req and a statement are both things that can have bycomps
has_bycomps = imp_req.statements if imp_req.statements else [imp_req]
for has_bycomp in has_bycomps:
statement_id = getattr(has_bycomp, 'statement_id', f'{control_id}_smt')
label = statement_id
part_name = None
# look up label for this statement
if control.parts:
found_label, part = self._catalog_interface.get_statement_label_if_exists(control_id, statement_id)
if found_label:
label = found_label
part_name = part.name
response_per_component = self._get_responses_by_components(has_bycomp, write_empty_responses)
if response_per_component or write_empty_responses:
if part_name and part_name == ITEM:
# print part header only if subitem
header = f'Implementation for part {label}'
md_writer.new_header(1, title=header)
for comp_name, comp_response in response_per_component.items():
if comp_name == SSP_MAIN_COMP_NAME:
continue
prose, rules, status = comp_response
if show_comp:
SSPMarkdownWriter._write_component_prompt(
md_writer, comp_name, prose, rules, status, show_rules, show_status
)
lines = md_writer.get_lines()
tree = DocsMarkdownNode.build_tree_from_markdown(lines)
tree.change_header_level_by(level)
return tree.content.raw_text
def _get_responses_by_components(self, has_bycomps: TypeWithByComps,
write_empty_responses: bool) -> Dict[str, Tuple[str, List[str], str]]:
"""Get response per component, substitute component id with title if possible."""
response_per_component: Dict[str, Tuple[str, str]] = {}
for by_comp in as_list(has_bycomps.by_components): # type: ignore
# look up component title
subheader = by_comp.component_uuid
prose = ''
status = ''
rules = []
if self._ssp.system_implementation.components:
for comp in self._ssp.system_implementation.components:
if comp.uuid == by_comp.component_uuid:
title = comp.title
if title:
subheader = title
if by_comp.description:
prose = by_comp.description
if by_comp.implementation_status:
status = by_comp.implementation_status.state
rules, _ = ControlInterface.get_rule_list_for_item(by_comp)
if prose or (not prose and write_empty_responses):
if subheader:
response_per_component[subheader] = (prose, rules, status)
return response_per_component
def _control_implemented_req(self, control_id: str) -> Optional[ssp.ImplementedRequirement]:
"""Retrieve control implemented requirement by control-id."""
requirements = self._ssp.control_implementation.implemented_requirements
for requirement in requirements:
if requirement.control_id == control_id:
return requirement
logger.debug(f'No implemented requirement found for control {control_id}')
return None
def _write_list_with_header(self, header: str, lines: List[str], level: int) -> str:
if lines:
md_writer = MDWriter(None)
md_writer.new_paragraph()
md_writer.new_header(level=1, title=header)
md_writer.set_indent_level(-1)
md_writer.new_list(lines)
md_writer.set_indent_level(-1)
return self._build_tree_and_adjust(md_writer.get_lines(), level)
return ''
def _write_table_with_header(
self, header: str, values: List[List[str]], table_header: List[str], level: int
) -> str:
if values and values[0]:
md_writer = MDWriter(None)
md_writer.new_paragraph()
md_writer.new_header(level=1, title=header)
md_writer.set_indent_level(-1)
md_writer.new_table(values, table_header)
md_writer.set_indent_level(-1)
return self._build_tree_and_adjust(md_writer.get_lines(), level)
return ''
def _write_str_with_header(self, header: str, text: str, level: int) -> str:
if text:
md_writer = MDWriter(None)
md_writer.new_paragraph()
md_writer.new_header(level=1, title=header)
md_writer.set_indent_level(-1)
md_writer.new_line(text)
md_writer.set_indent_level(-1)
return self._build_tree_and_adjust(md_writer.get_lines(), level)
return ''
def _build_tree_and_adjust(self, lines: List[str], level: int) -> str:
tree = DocsMarkdownNode.build_tree_from_markdown(lines)
tree.change_header_level_by(level)
return tree.content.raw_text
|