35
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 | class DescribeCmd(CommandPlusDocs):
"""Describe contents of a model file including optional element path."""
# The only output is via log lines. No other results or side-effects.
name = 'describe'
def _init_arguments(self) -> None:
logger.debug('Init arguments')
self.add_argument('-f', '--file', help='OSCAL file to import.', type=str, required=True)
self.add_argument(
'-e', '--element', help='Optional name of element in file to describe.', type=str, required=False
)
def _run(self, args: argparse.Namespace) -> int:
try:
logger.debug('Entering trestle describe.')
log.set_log_level_from_args(args)
if args.file:
model_file = pathlib.Path(args.file)
element = '' if not args.element else args.element.strip("'")
results = self.describe(model_file.resolve(), element, args.trestle_root)
return CmdReturnCodes.SUCCESS.value if len(results) > 0 else CmdReturnCodes.COMMAND_ERROR.value
else:
raise TrestleIncorrectArgsError('No file specified for command describe.')
except Exception as e: # pragma: no cover
return handle_generic_command_exception(e, logger, 'Error while describing contents of a model')
@classmethod
def _clean_type_string(cls, text: str) -> str:
text = text.replace("<class '", '').replace("'>", '')
text = text.replace('trestle.oscal.', '')
text = text.replace('pydantic.main.', 'stripped.')
return text
@classmethod
def _description_text(cls, sub_model: Optional[Union[OscalBaseModel, List[OscalBaseModel]]]) -> str:
clip_string = 100
if sub_model is None:
return 'None'
if type(sub_model) is list:
n_items = len(sub_model)
type_text = 'Unknown' if not n_items else f'{cls._clean_type_string(str(type(sub_model[0])))}'
text = f'list of {n_items} items of type {type_text}'
return text
if type(sub_model) is str:
return sub_model if len(sub_model
) < clip_string else sub_model[:clip_string] + '[truncated]' # type: ignore
if hasattr(sub_model, 'type_'):
return cls._clean_type_string(str(sub_model.type_))
return cls._clean_type_string(str(type(sub_model)))
@classmethod
def describe(cls, file_path: pathlib.Path, element_path_str: str, trestle_root: pathlib.Path) -> List[str]:
"""Describe the contents of the file.
Args:
file_path: pathlib.Path Path for model file to describe.
element_path_str: Element path of element in model to describe. Can be ''.
Returns:
The list of lines of text in the description, or an empty list on failure
"""
# figure out the model type so we can read it
try:
model_type, _ = ModelUtils.get_stripped_model_type(file_path, trestle_root)
model: OscalBaseModel = model_type.oscal_read(file_path)
except TrestleError as e:
logger.warning(f'Error loading model {file_path} to describe: {e}')
return []
sub_model = model
# if an element path was provided, follow the path chain to the desired sub_model
if element_path_str:
if '*' in element_path_str or ',' in element_path_str:
logger.warning('Wildcards and commas are not allowed in the element path for describe.')
return []
if '.' not in element_path_str:
logger.warning('The element path for describe must either be omitted or contain at least 2 parts.')
return []
element_paths = utils.parse_element_arg(model, element_path_str)
sub_model_element = Element(model)
for element_path in element_paths:
sub_model = sub_model_element.get_at(element_path, False)
sub_model_element = Element(sub_model)
# now that we have the desired sub_model we can describe it
text_out: List[str] = []
# create top level text depending on whether an element path was used
element_text = '' if not element_path_str else f' at element path {element_path_str}'
if type(sub_model) is list:
text = f'Model file {file_path}{element_text} is a {cls._description_text(sub_model)}'
text_out.append(text)
logger.info(text)
else:
text = f'Model file {file_path}{element_text} is of type '
text += f'{cls._clean_type_string(str(type(sub_model)))} and contains:'
text_out.append(text)
logger.info(text)
for key in sub_model.__fields__.keys():
value = getattr(sub_model, key, None)
text = f' {key}: {cls._description_text(value)}'
text_out.append(text)
logger.info(text)
return text_out
|