Coverage for muuntaa/cli.py: 90.00%
62 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-21 12:16:47 +00:00
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-21 12:16:47 +00:00
1import argparse
2import logging
3import pathlib
4import sys
5from typing import Union
7import muuntaa.advisor as advisor
8import muuntaa.config as cfg
9import muuntaa.writer as writer
10from muuntaa import (
11 APP_ALIAS,
12 APP_NAME,
13 ConfigType,
14 ENCODING,
15 INPUT_FILE_KEY,
16 OVERWRITABLE_KEYS,
17 ScopedMessages,
18 VERSION,
19 log,
20)
22FALLBACK_CVSS3_VERSION = '3.0'
23MAGIC_CMD_ARG_ENTERED = 'cmd-arg-entered'
25scoped_log = log.log # noqa
28def parse_request(argv: Union[list[str], None] = None) -> tuple[Union[int, ConfigType], ScopedMessages]:
29 """Parse the request as load configuration and mix in (overwrite) command line parameter values."""
30 if argv is None:
31 argv = sys.argv[1:] # pragma: no cover
32 parser = argparse.ArgumentParser(
33 prog=APP_ALIAS, description=APP_NAME, formatter_class=argparse.RawTextHelpFormatter
34 )
35 # General args
36 parser.add_argument('-v', '--version', action='version', version=VERSION)
37 parser.add_argument(
38 '--input-file', dest='input_file', type=str, required=True, help='CVRF XML input file to parse', metavar='PATH'
39 )
40 parser.add_argument(
41 '--output-dir',
42 dest='output_dir',
43 type=str,
44 default='./',
45 metavar='PATH',
46 help='CSAF output dir to write to. Filename is derived from /document/tracking/id.',
47 )
48 parser.add_argument(
49 '--print',
50 dest='print',
51 action='store_true',
52 default=False,
53 help='Additionally prints CSAF JSON output on stdout.',
54 )
55 parser.add_argument(
56 '--force',
57 action='store_const',
58 const='cmd-arg-entered',
59 help=(
60 'If used, the converter produces output even if it is invalid (errors occurred during conversion).\n'
61 'Target use case: best-effort conversion to JSON, fix the errors manually, e.g. in Secvisogram.'
62 ),
63 )
65 # Document Publisher args
66 parser.add_argument('--publisher-name', dest='publisher_name', type=str, help='Name of the publisher.')
67 parser.add_argument(
68 '--publisher-namespace',
69 dest='publisher_namespace',
70 type=str,
71 help='Namespace of the publisher. Must be a valid URI',
72 )
74 # Document Tracking args
75 parser.add_argument(
76 '--fix-insert-current-version-into-revision-history',
77 action='store_const',
78 const='cmd-arg-entered',
79 help=(
80 'If the current version is not present in the revision history the current version is\n'
81 'added to the revision history. Also, warning is produced. By default, an error is produced.'
82 ),
83 )
85 # Document References args
86 parser.add_argument(
87 '--force-insert-default-reference-category',
88 action='store_const',
89 const='cmd-arg-entered',
90 help="When 'Type' attribute not present in 'Reference' element, then force using default value 'external'.",
91 )
93 # Vulnerabilities args
94 parser.add_argument(
95 '--remove-CVSS-values-without-vector',
96 action='store_const',
97 const='cmd-arg-entered',
98 help=(
99 'If vector is not present in CVSS ScoreSet,\n'
100 'the convertor removes the whole ScoreSet instead of producing an error.'
101 ),
102 )
104 parser.add_argument(
105 '--default-CVSS3-version',
106 dest='default_CVSS3_version',
107 help=(
108 'Default version used for CVSS version 3, when the version cannot be derived from other sources.\n'
109 f"Default value is '{FALLBACK_CVSS3_VERSION}'."
110 ),
111 )
113 try:
114 args = {k: v for k, v in vars(parser.parse_args(argv)).items() if v is not None}
115 except SystemExit as err:
116 return int(str(err)), []
118 config = cfg.load()
119 scoped_messages = cfg.boolify(config)
120 for scope, message in scoped_messages: 120 ↛ 121line 120 didn't jump to line 121 because the loop on line 120 never started
121 scoped_log(scope, message)
122 if scope >= logging.CRITICAL:
123 return 1, []
125 config.update(args) # Update and overwrite config file values with the ones from command line arguments
126 for key in OVERWRITABLE_KEYS: # Boolean optional arguments that are also present in config need special treatment
127 if config.get(key) == MAGIC_CMD_ARG_ENTERED:
128 config[key] = True
130 if not pathlib.Path(config.get(INPUT_FILE_KEY, '')).is_file(): # type: ignore
131 # Avoided type error using empty string as default, which fakes missing file per current dir
132 scoped_log(logging.CRITICAL, f'Input file not found, check the path: {config.get(INPUT_FILE_KEY)}')
133 return 1, []
135 return config, []
138def process(configuration: ConfigType) -> int:
139 """Visit the source and yield the requested transformed target."""
140 in_path = pathlib.Path(configuration[INPUT_FILE_KEY]) # type: ignore
141 with open(in_path, 'r', encoding=ENCODING) as source:
142 loaded = source.read()
144 csaf_dict: dict[str, object] = {'csaf_version': '2.0', 'incoming_blob': loaded}
146 out_path = advisor.derive_csaf_filename()
147 scoped_messages = writer.write_csaf(csaf_dict, out_path)
148 for scope, message in scoped_messages:
149 scoped_log(scope, message)
150 if scope >= logging.CRITICAL: 150 ↛ 151line 150 didn't jump to line 151 because the condition on line 150 was never true
151 return 1
153 return 0
156def app(argv: Union[list[str], None] = None) -> int:
157 """Delegate processing to functional module."""
158 argv = sys.argv[1:] if argv is None else argv
159 configuration, scoped_messages = parse_request(argv)
160 if isinstance(configuration, int):
161 return 0
162 return process(configuration)