Test Failed
Push — master ( 8e3f9b...05d38f )
by
unknown
03:43 queued 12s
created

utils.controleval   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 237
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 175
dl 0
loc 237
ccs 0
cts 119
cp 0
rs 10
c 0
b 0
f 0
wmc 17

9 Functions

Rating   Name   Duplication   Size   Complexity  
A print_to_json() 0 39 2
A create_parser() 0 28 1
A main() 0 10 1
A get_id_array() 0 2 1
B calculate_stats() 0 50 3
A print_options() 0 5 2
A stats() 0 17 3
A print_specific_stat() 0 6 1
A validate_args() 0 18 3
1
#!/usr/bin/python3
2
import collections
3
import argparse
4
import os
5
import json
6
7
# NOTE: This is not to be confused with the https://pypi.org/project/ssg/
8
# package. The ssg package we're referencing here is actually a relative import
9
# within this repository. Because of this, you need to ensure
10
# ComplianceAsCode/content/ssg is discoverable from PYTHONPATH before you
11
# invoke this script.
12
try:
13
    from ssg import controls
14
    import ssg.products
15
except ModuleNotFoundError as e:
16
    # NOTE: Only emit this message if we're dealing with an import error for
17
    # ssg. Since the local ssg module imports other things, like PyYAML, we
18
    # don't want to emit misleading errors for legit dependencies issues if the
19
    # user hasn't installed PyYAML or other transitive dependencies from ssg.
20
    # We should revisit this if or when we decide to implement a python package
21
    # management strategy for the python scripts provided in this repository.
22
    if e.name == 'ssg':
23
        msg = """Unable to import local 'ssg' module.
24
25
The 'ssg' package from within this repository must be discoverable before
26
invoking this script. Make sure the top-level directory of the
27
ComplianceAsCode/content repository is available in the PYTHONPATH environment
28
variable (example: $ export PYTHONPATH=($pwd)).
29
"""
30
        raise RuntimeError(msg) from e
31
    raise
32
33
34
SSG_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
35
36
37
def print_options(opts):
38
    if len(opts) > 0:
39
        print("Available options are:\n - " + "\n - ".join(opts))
40
    else:
41
        print("The controls file is not written appropriately.")
42
43
44
def validate_args(ctrlmgr, args):
45
    """ Validates that the appropriate args were given
46
        and that they're valid entries in the control manager."""
47
48
    policy = None
49
    try:
50
        policy = ctrlmgr._get_policy(args.id)
51
    except ValueError as e:
52
        print("Error: ", e)
53
        print_options(ctrlmgr.policies.keys())
54
        exit(1)
55
56
    try:
57
        policy.get_level_with_ancestors_sequence(args.level)
58
    except ValueError as e:
59
        print("Error: ", e)
60
        print_options(policy.levels_by_id.keys())
61
        exit(1)
62
63
64
def calculate_stats(ctrls):
65
    total = len(ctrls)
66
    ctrlstats = collections.defaultdict(int)
67
    ctrllist = collections.defaultdict(set)
68
69
    if total == 0:
70
        print("No controls founds with the given inputs. Maybe try another level.")
71
        exit(1)
72
73
    for ctrl in ctrls:
74
        ctrlstats[str(ctrl.status)] += 1
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable str does not seem to be defined.
Loading history...
75
        ctrllist[str(ctrl.status)].add(ctrl)
76
77
    applicable = total - ctrlstats[controls.Status.NOT_APPLICABLE]
78
    assessed = ctrlstats[controls.Status.AUTOMATED] + ctrlstats[controls.Status.SUPPORTED] + \
79
        ctrlstats[controls.Status.DOCUMENTATION] + ctrlstats[controls.Status.INHERENTLY_MET] + \
80
        ctrlstats[controls.Status.PARTIAL]
81
82
    print("Total controls = {total}".format(total=total))
83
    print_specific_stat("Applicable", applicable, total)
84
    print_specific_stat("Assessed", assessed, applicable)
85
    print()
86
    print_specific_stat("Automated",
87
                        ctrlstats[controls.Status.AUTOMATED],
88
                        applicable)
89
    print_specific_stat("Manual",
90
                        ctrlstats[controls.Status.MANUAL],
91
                        applicable)
92
    print_specific_stat("Supported",
93
                        ctrlstats[controls.Status.SUPPORTED],
94
                        applicable)
95
    print_specific_stat("Documentation",
96
                        ctrlstats[controls.Status.DOCUMENTATION],
97
                        applicable)
98
    print_specific_stat("Inherently Met",
99
                        ctrlstats[controls.Status.INHERENTLY_MET],
100
                        applicable)
101
    print_specific_stat("Does Not Meet",
102
                        ctrlstats[controls.Status.DOES_NOT_MEET],
103
                        applicable)
104
    print_specific_stat("Partial",
105
                        ctrlstats[controls.Status.PARTIAL],
106
                        applicable)
107
108
    applicablelist = ctrls - ctrllist[controls.Status.NOT_APPLICABLE]
109
    assessedlist = set().union(ctrllist[controls.Status.AUTOMATED]).union(ctrllist[controls.Status.SUPPORTED])\
110
        .union(ctrllist[controls.Status.DOCUMENTATION]).union(ctrllist[controls.Status.INHERENTLY_MET])\
111
        .union(ctrllist[controls.Status.PARTIAL]).union(ctrllist[controls.Status.MANUAL])
112
    print("Missing:", ", ".join(sorted(str(c.id)
113
          for c in applicablelist - assessedlist)))
114
115
116
def print_specific_stat(stat, current, total):
117
    print("{stat} = {percent}% -- {current} / {total}".format(
118
        stat=stat,
119
        percent=round((current / total) * 100.00, 2),
120
        current=current,
121
        total=total))
122
123
124
def stats(ctrlmgr, args):
125
    validate_args(ctrlmgr, args)
126
    ctrls = set(ctrlmgr.get_all_controls_of_level(args.id, args.level))
127
    total = len(ctrls)
128
129
    if total == 0:
130
        print("No controls founds with the given inputs. Maybe try another level.")
131
        exit(1)
132
133
    if args.output_format == 'json':
134
        print_to_json(
135
            ctrls,
136
            args.product,
137
            args.id,
138
            args.level)
139
    else:
140
        calculate_stats(ctrls)
141
142
143
def print_to_json(ctrls, product, id, level):
144
    data = dict()
145
    ctrllist = collections.defaultdict(set)
146
147
    for ctrl in ctrls:
148
        ctrllist[str(ctrl.status)].add(ctrl)
149
150
    applicablelist = ctrls - ctrllist[controls.Status.NOT_APPLICABLE]
151
    assessedlist = set().union(ctrllist[controls.Status.AUTOMATED]).union(ctrllist[controls.Status.SUPPORTED])\
152
        .union(ctrllist[controls.Status.DOCUMENTATION]).union(ctrllist[controls.Status.INHERENTLY_MET])\
153
        .union(ctrllist[controls.Status.PARTIAL]).union(ctrllist[controls.Status.MANUAL])
154
155
    data["format_version"] = "v0.0.1"
156
    data["product_name"] = product
157
    data["benchmark"] = dict()
158
    data["benchmark"]["name"] = id
159
    data["benchmark"]["baseline"] = level
160
    data["total_controls"] = len(applicablelist)
161
    data["addressed_controls"] = dict()
162
    data["addressed_controls"]["all"] = get_id_array(ctrls)
163
    data["addressed_controls"]["applicable"] = get_id_array(applicablelist)
164
    data["addressed_controls"]["assessed"] = get_id_array(assessedlist)
165
    data["addressed_controls"]["inherently"] = get_id_array(
166
        ctrllist[controls.Status.INHERENTLY_MET])
167
    data["addressed_controls"]["manual"] = get_id_array(
168
        ctrllist[controls.Status.MANUAL])
169
    data["addressed_controls"]["supported"] = get_id_array(
170
        ctrllist[controls.Status.SUPPORTED])
171
    data["addressed_controls"]["automated"] = get_id_array(
172
        ctrllist[controls.Status.AUTOMATED])
173
    data["addressed_controls"]["notapplicable"] = get_id_array(
174
        ctrllist[controls.Status.NOT_APPLICABLE])
175
    data["addressed_controls"]["partial"] = get_id_array(
176
        ctrllist[controls.Status.PARTIAL])
177
    data["addressed_controls"]["pending"] = get_id_array(
178
        ctrllist[controls.Status.PENDING])
179
    data["addressed_controls"]["notassessed"] = get_id_array(
180
        applicablelist - assessedlist)
181
    print(json.dumps(data))
182
183
184
subcmds = dict(
185
    stats=stats
186
)
187
188
189
def get_id_array(ctrls):
190
    return [c.id for c in ctrls]
191
192
193
def create_parser():
194
    parser = argparse.ArgumentParser()
195
    parser.add_argument(
196
        "--controls-dir",
197
        help=("Directory that contains control files with policy controls. "
198
              "e.g.: ~/scap-security-guide/controls"),
199
        default="./controls/",
200
    )
201
    subparsers = parser.add_subparsers(dest="subcmd", required=True)
202
    statsparser = subparsers.add_parser(
203
        'stats',
204
        help="calculate and return the statistics for the given benchmark")
205
    statsparser.add_argument(
206
        "-i", "--id",
207
        help="the ID or name of the controls file in the controls/ directory",
208
        required=True)
209
    statsparser.add_argument(
210
        "-l", "--level",
211
        help="the compliance target level to analyze",
212
        required=True)
213
    statsparser.add_argument(
214
        "-p", "--product", help="product to check has required references")
215
    statsparser.add_argument(
216
        "-o",
217
        "--output-format",
218
        choices=['json'],
219
        help="The output format of the report")
220
    return parser
221
222
223
def main():
224
    parser = create_parser()
225
    args = parser.parse_args()
226
    product_base = os.path.join(SSG_ROOT, "products", args.product)
227
    product_yaml = os.path.join(product_base, "product.yml")
228
    env_yaml = ssg.products.load_product_yaml(product_yaml)
229
    controls_manager = controls.ControlsManager(
230
        args.controls_dir, env_yaml=env_yaml)
231
    controls_manager.load()
232
    subcmds[args.subcmd](controls_manager, args)
233
234
235
if __name__ == "__main__":
236
    main()
237