Skip to content

Commit 526ba30

Browse files
mikeurbanski1gruebeltsmithv11
authored
feat(general): Normalize framework flags (bridgecrewio#94)
* change --framework to use action append and CSV * log config * normalize different ways of specifying frameworks * add framework normalization tests * reuse framework parsing logic for both flags * fix lint issues * fix lint issues * Update checkov/common/util/ext_argument_parser.py Co-authored-by: Anton Grübel <[email protected]> * Update checkov/common/util/ext_argument_parser.py Co-authored-by: Taylor <[email protected]> * Update checkov/main.py Co-authored-by: Anton Grübel <[email protected]> * Update checkov/main.py Co-authored-by: Anton Grübel <[email protected]> --------- Co-authored-by: Anton Grübel <[email protected]> Co-authored-by: Taylor <[email protected]>
1 parent e7db8ea commit 526ba30

File tree

3 files changed

+183
-13
lines changed

3 files changed

+183
-13
lines changed

checkov/common/util/ext_argument_parser.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from __future__ import annotations
22

33
from io import StringIO
4-
from typing import Any, TYPE_CHECKING, cast
4+
from typing import Any, TYPE_CHECKING, cast, List
55

66
import configargparse
77

8-
from checkov.common.bridgecrew.check_type import checkov_runners, sast_types
8+
from checkov.common.bridgecrew.check_type import checkov_runners
99
from checkov.common.runners.runner_registry import OUTPUT_CHOICES, SUMMARY_POSITIONS
1010
from checkov.common.util.consts import DEFAULT_EXTERNAL_MODULES_DIR
1111
from checkov.common.util.type_forcers import convert_str_to_bool
@@ -15,6 +15,18 @@
1515
import argparse
1616

1717

18+
def flatten_csv(list_to_flatten: List[List[str]]) -> List[str]:
19+
"""
20+
Flattens a list of list of strings into a list of strings, while also splitting out comma-separated values
21+
Duplicates will be removed.
22+
[['terraform', 'arm'], ['bicep,cloudformation,arm']] -> ['terraform', 'arm', 'bicep', 'cloudformation']
23+
(Order is not guaranteed)
24+
"""
25+
if not list_to_flatten:
26+
return []
27+
return list({s for sublist in list_to_flatten for val in sublist for s in val.split(',')})
28+
29+
1830
class ExtArgumentParser(configargparse.ArgumentParser):
1931
def __init__(self, *args: Any, **kwargs: Any) -> None:
2032
super().__init__(*args, **kwargs)
@@ -211,21 +223,26 @@ def add_parser_args(self) -> None:
211223
)
212224
self.add(
213225
"--framework",
214-
help="Filter scan to run only on specific infrastructure code frameworks",
215-
choices=checkov_runners + sast_types + ["all"],
216-
default=["all"],
226+
help="Filter scan to run only on specific infrastructure as code frameworks. Defaults to all frameworks. If you "
227+
"explicitly include 'all' as a value, then all other values are ignored. Enter as a "
228+
"comma-separated list or repeat the flag multiple times. For example, --framework terraform,sca_package "
229+
f"or --framework terraform --framework sca_package. Possible values: {', '.join(['all'] + checkov_runners)}",
217230
env_var="CKV_FRAMEWORK",
218-
nargs="+",
231+
action='append',
232+
nargs='+' # we will still allow the old way (eg: --framework terraform arm cloudformation), just not prefer it
233+
# intentionally no default value - we will set it explicitly during normalization (it messes up the list of lists)
219234
)
220235
self.add(
221236
"--skip-framework",
222-
help="Filter scan to skip specific infrastructure as code frameworks."
237+
help="Filter scan to skip specific infrastructure as code frameworks. "
223238
"This will be included automatically for some frameworks if system dependencies "
224-
"are missing. Add multiple frameworks using spaces. For example, "
225-
"--skip-framework terraform sca_package.",
226-
choices=checkov_runners,
239+
"are missing. Enter as a comma-separated list or repeat the flag multiple times. For example, "
240+
"--skip-framework terraform,sca_package or --skip-framework terraform --skip-framework sca_package. "
241+
"Cannot include values that are also included in --framework. "
242+
f"Possible values: {', '.join(checkov_runners)}",
227243
default=None,
228-
nargs="+",
244+
action='append',
245+
nargs='+'
229246
)
230247
self.add(
231248
"-c",

checkov/main.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import sys
1313
from collections import defaultdict
1414
from pathlib import Path
15-
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, List
15+
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, List, cast
1616

1717
import argcomplete
1818
import configargparse
@@ -53,7 +53,7 @@
5353
from checkov.common.util.banner import banner as checkov_banner, tool as checkov_tool
5454
from checkov.common.util.config_utils import get_default_config_paths
5555
from checkov.common.util.consts import CHECKOV_RUN_SCA_PACKAGE_SCAN_V2
56-
from checkov.common.util.ext_argument_parser import ExtArgumentParser
56+
from checkov.common.util.ext_argument_parser import ExtArgumentParser, flatten_csv
5757
from checkov.common.util.runner_dependency_handler import RunnerDependencyHandler
5858
from checkov.common.util.type_forcers import convert_str_to_bool
5959
from checkov.contributor_metrics import report_contributor_metrics
@@ -206,13 +206,49 @@ def normalize_config(self) -> None:
206206
'--policy-metadata-filter flag was used without a Prisma Cloud API key. Policy filtering will be skipped.'
207207
)
208208

209+
logging.debug('Normalizing --framework')
210+
self.config.framework = self.normalize_framework_arg(self.config.framework, handle_all=True)
211+
logging.debug(f'Normalized --framework value: {self.config.framework}')
212+
213+
logging.debug('Normalizing --skip-framework')
214+
self.config.skip_framework = self.normalize_framework_arg(self.config.skip_framework)
215+
logging.debug(f'Normalized --skip-framework value: {self.config.skip_framework}')
216+
217+
duplicate_frameworks = set(self.config.skip_framework).intersection(self.config.framework)
218+
if duplicate_frameworks:
219+
self.parser.error(f'Frameworks listed for both --framework and --skip-framework: {", ".join(duplicate_frameworks)}')
220+
209221
# Parse mask into json with default dict. If self.config.mask is empty list, default dict will be assigned
210222
self._parse_mask_to_resource_attributes_to_omit()
211223

212224
if self.config.file:
213225
# it is passed as a list of lists
214226
self.config.file = list(itertools.chain.from_iterable(self.config.file))
215227

228+
def normalize_framework_arg(self, raw_framework_arg: List[List[str]], handle_all=False) -> List[str]:
229+
# frameworks come as arrays of arrays, e.g. --framework terraform arm --framework bicep,cloudformation
230+
# becomes: [['terraform', 'arm'], ['bicep,cloudformation']]
231+
# we'll collapse it into a single array (which is how it was before checkov3)
232+
233+
if raw_framework_arg:
234+
logging.debug(f'Raw framework value: {raw_framework_arg}')
235+
frameworks = flatten_csv(cast(List[List[str]], raw_framework_arg))
236+
logging.debug(f'Flattened frameworks: {frameworks}')
237+
if handle_all and 'all' in frameworks:
238+
return ['all']
239+
else:
240+
invalid = list(filter(lambda f: f not in checkov_runners, frameworks))
241+
if invalid:
242+
self.parser.error(f'Invalid frameworks specified: {", ".join(invalid)}.{os.linesep}'
243+
f'Valid values are: {", ".join(checkov_runners + ["all"] if handle_all else [])}')
244+
return frameworks
245+
elif handle_all:
246+
logging.debug('No framework specified; setting to `all`')
247+
return ['all']
248+
else:
249+
logging.debug('No framework specified; setting to none')
250+
return []
251+
216252
def run(self, banner: str = checkov_banner, tool: str = checkov_tool, source_type: SourceType | None = None) -> int | None:
217253
self.run_metadata = {
218254
"checkov_version": version,

tests/config/TestCLIArgs.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import unittest
2+
3+
from checkov.main import Checkov
4+
5+
6+
class ConfigException(Exception):
7+
pass
8+
9+
10+
# override parser.error, which prints the error and exits
11+
def parser_error(message: str):
12+
raise ConfigException(message)
13+
14+
15+
class TestCLIArgs(unittest.TestCase):
16+
def test_normalize_frameworks(self):
17+
argv = []
18+
ckv = Checkov(argv=argv)
19+
self.assertEqual(ckv.config.framework, ['all'])
20+
self.assertEqual(ckv.config.skip_framework, [])
21+
22+
argv = ['--framework', 'terraform']
23+
ckv = Checkov(argv=argv)
24+
self.assertEqual(ckv.config.framework, ['terraform'])
25+
26+
argv = ['--framework', 'terraform,arm']
27+
ckv = Checkov(argv=argv)
28+
self.assertEqual(set(ckv.config.framework), {'terraform', 'arm'})
29+
30+
argv = ['--framework', 'terraform', 'arm']
31+
ckv = Checkov(argv=argv)
32+
self.assertEqual(set(ckv.config.framework), {'terraform', 'arm'})
33+
34+
argv = ['--framework', 'terraform', '--framework', 'arm']
35+
ckv = Checkov(argv=argv)
36+
self.assertEqual(set(ckv.config.framework), {'terraform', 'arm'})
37+
38+
argv = ['--framework', 'terraform,bicep', '--framework', 'arm']
39+
ckv = Checkov(argv=argv)
40+
self.assertEqual(set(ckv.config.framework), {'terraform', 'arm', 'bicep'})
41+
42+
argv = ['--framework', 'terraform,bicep', '--framework', 'arm,all']
43+
ckv = Checkov(argv=argv)
44+
self.assertEqual(ckv.config.framework, ['all'])
45+
46+
argv = ['--framework', 'terraform,bicep', '--framework', 'arm,invalid']
47+
ckv = Checkov(argv=[]) # first instantiate a valid one
48+
# now repeat some of the logic of the constructor, overriding values
49+
ckv.config = ckv.parser.parse_args(argv)
50+
ckv.parser.error = parser_error
51+
with self.assertRaises(ConfigException):
52+
ckv.normalize_config()
53+
54+
# all is specified, so we do not expect an exception
55+
argv = ['--framework', 'terraform,bicep', '--framework', 'arm,invalid,all']
56+
ckv = Checkov(argv=argv)
57+
self.assertEqual(ckv.config.framework, ['all'])
58+
59+
def test_normalize_skip_frameworks(self):
60+
argv = ['--skip-framework', 'terraform']
61+
ckv = Checkov(argv=argv)
62+
self.assertEqual(ckv.config.skip_framework, ['terraform'])
63+
64+
argv = ['--skip-framework', 'terraform,arm']
65+
ckv = Checkov(argv=argv)
66+
self.assertEqual(set(ckv.config.skip_framework), {'terraform', 'arm'})
67+
68+
argv = ['--skip-framework', 'terraform', 'arm']
69+
ckv = Checkov(argv=argv)
70+
self.assertEqual(set(ckv.config.skip_framework), {'terraform', 'arm'})
71+
72+
argv = ['--skip-framework', 'terraform', '--skip-framework', 'arm']
73+
ckv = Checkov(argv=argv)
74+
self.assertEqual(set(ckv.config.skip_framework), {'terraform', 'arm'})
75+
76+
argv = ['--skip-framework', 'terraform,bicep', '--skip-framework', 'arm']
77+
ckv = Checkov(argv=argv)
78+
self.assertEqual(set(ckv.config.skip_framework), {'terraform', 'arm', 'bicep'})
79+
80+
# all is not allowed
81+
argv = ['--skip-framework', 'terraform,bicep', '--skip-framework', 'arm,all']
82+
ckv = Checkov(argv=[])
83+
ckv.config = ckv.parser.parse_args(argv)
84+
ckv.parser.error = parser_error
85+
with self.assertRaises(ConfigException):
86+
ckv.normalize_config()
87+
88+
argv = ['--skip-framework', 'terraform,bicep', '--skip-framework', 'arm,invalid']
89+
ckv = Checkov(argv=[])
90+
ckv.config = ckv.parser.parse_args(argv)
91+
ckv.parser.error = parser_error
92+
with self.assertRaises(ConfigException):
93+
ckv.normalize_config()
94+
95+
def test_combine_framework_and_skip(self):
96+
argv = ['--framework', 'terraform', '--skip-framework', 'arm']
97+
ckv = Checkov(argv=argv)
98+
self.assertEqual(ckv.config.framework, ['terraform'])
99+
self.assertEqual(ckv.config.skip_framework, ['arm'])
100+
101+
# duplicate values not allowed
102+
argv = ['--framework', 'arm', '--skip-framework', 'arm']
103+
ckv = Checkov(argv=[])
104+
ckv.config = ckv.parser.parse_args(argv)
105+
ckv.parser.error = parser_error
106+
with self.assertRaises(ConfigException):
107+
ckv.normalize_config()
108+
109+
# but it works with all
110+
argv = ['--framework', 'arm,all', '--skip-framework', 'arm']
111+
ckv = Checkov(argv=argv)
112+
self.assertEqual(ckv.config.framework, ['all'])
113+
self.assertEqual(ckv.config.skip_framework, ['arm'])
114+
115+
116+
if __name__ == '__main__':
117+
unittest.main()

0 commit comments

Comments
 (0)