-
Notifications
You must be signed in to change notification settings - Fork 1
/
sca_check.py
executable file
·2083 lines (1755 loc) · 63.5 KB
/
sca_check.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
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
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
Author: Elyasnz
GitHub: https://github.com/Elyasnz
Date: 2024-06-04
Description: Security Configuration Assessment (SCA) script for Wazuh. This script automates the process of
analyzing system configurations and applying solutions to ensure compliance with security standards.
Compliance Automation Script
This script is designed to automate compliance checks and remediation actions
based on a set of predefined rules and solutions. The script leverages a YAML
configuration file to define various compliance checks, each of which can
include file existence checks, directory checks, and command execution checks.
Key Components:
- Utilities for text formatting and command execution.
- Rule parsing and evaluation for compliance checks.
- Automated application of remediation actions based on check results.
- Support for YAML configuration to load checks and solutions.
Usage:
1. Load compliance checks and solutions from a YAML file.
2. Execute all compliance checks and collect results.
3. Optionally apply available remediation actions for failed checks.
Example:
python sca_check.py cis_path [solutions_path] [whitelisted_checks]
Dependencies:
- pyyaml: For loading YAML configuration files.
# todo handle cis variables
"""
import operator
import re
import urllib.request
from os import popen, listdir, system, geteuid
from pathlib import Path
from subprocess import call
from sys import argv
from yaml import load as yaml_load, Loader as YamlLoader
reboot_required = False
# region Utils
class FormatText:
"""
Text terminal formatter.
Provides methods to format text for terminal output using ANSI escape codes.
For more information, see:
https://en.wikipedia.org/wiki/ANSI_escape_code
https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
"""
tag = "\x1b" # ANSI escape character
# region format
format_bold = "1"
format_dim = "2"
format_italic = "3"
format_underline = "4"
format_blink = "5"
format_reverse = "7"
format_hide = "8"
format_cross = "9"
format_box = "52"
# endregion
# region color foreground
color_f_black = "30"
color_f_red = "31"
color_f_green = "32"
color_f_yellow = "33"
color_f_blue = "34"
color_f_magenta = "35"
color_f_cyan = "36"
color_f_white = "37"
color_f_black_bright = "90"
color_f_red_bright = "91"
color_f_green_bright = "92"
color_f_yellow_bright = "93"
color_f_blue_bright = "94"
color_f_magenta_bright = "95"
color_f_cyan_bright = "96"
color_f_white_bright = "97"
@classmethod
def color_f_rgb(cls, r, g, b):
"""
Generates an ANSI escape code for setting the foreground color to an RGB value.
@param r: Red component (0-255).
@type r: int
@param g: Green component (0-255).
@type g: int
@param b: Blue component (0-255).
@type b: int
@return: ANSI escape code for RGB foreground color.
@rtype: str
"""
return f"38;2;{r};{g};{b}"
# endregion
# region color background
color_b_black = "40"
color_b_red = "41"
color_b_green = "42"
color_b_yellow = "43"
color_b_blue = "44"
color_b_magenta = "45"
color_b_cyan = "46"
color_b_white = "47"
color_b_black_bright = "100"
color_b_red_bright = "101"
color_b_green_bright = "102"
color_b_yellow_bright = "103"
color_b_blue_bright = "104"
color_b_magenta_bright = "105"
color_b_cyan_bright = "106"
color_b_white_bright = "107"
@classmethod
def color_b_rgb(cls, r, g, b):
"""
Generates an ANSI escape code for setting the background color to an RGB value.
@param r: Red component (0-255).
@type r: int
@param g: Green component (0-255).
@type g: int
@param b: Blue component (0-255).
@type b: int
@return: ANSI escape code for RGB background color.
@rtype: str
"""
return f"48;2;{r};{g};{b}"
# endregion
# region interact
interact_clear_screen = "2J"
interact_clear_screen_and_buffer = "3J"
interact_clear_screen_from_cursor = "1J"
interact_clear_line = "2K"
interact_clear_line_from_cursor = "K"
interact_save_cursor_pos = "s"
interact_restore_cursor_pos = "u"
interact_show_cursor = "?25h"
interact_hide_cursor = "?25l"
@classmethod
def interact_cursor_up(cls, cells=1):
"""
Moves the cursor up by the specified number of cells.
Abbr: CUU
@param cells: Number of cells to move up. Defaults to 1.
@type cells: int
@return: ANSI escape code for moving cursor up.
@rtype: str
"""
return f"{cells}A"
@classmethod
def interact_cursor_down(cls, cells=1):
"""
Moves the cursor down by the specified number of cells.
Abbr: CUD
@param cells: Number of cells to move down. Defaults to 1.
@type cells: int
@return: ANSI escape code for moving cursor down.
@rtype: str
"""
return f"{cells}B"
@classmethod
def interact_cursor_forward(cls, cells=1):
"""
Moves the cursor forward by the specified number of cells.
Abbr: CUF
@param cells: Number of cells to move forward. Defaults to 1.
@type cells: int
@return: ANSI escape code for moving cursor forward.
@rtype: str
"""
return f"{cells}C"
@classmethod
def interact_cursor_back(cls, cells=1):
"""
Moves the cursor back by the specified number of cells.
Abbr: CUB
@param cells: Number of cells to move back. Defaults to 1.
@type cells: int
@return: ANSI escape code for moving cursor back.
@rtype: str
"""
return f"{cells}D"
@classmethod
def interact_cursor_next_line(cls, lines=1):
"""
Moves the cursor to the beginning of the specified number of next lines.
Abbr: CNL
@param lines: Number of lines to move down. Defaults to 1.
@type lines: int
@return: ANSI escape code for moving cursor to the next line.
@rtype: str
"""
return f"{lines}E"
@classmethod
def interact_cursor_prev_line(cls, lines=1):
"""
Moves the cursor to the beginning of the specified number of previous lines.
Abbr: CPL
@param lines: Number of lines to move up. Defaults to 1.
@type lines: int
@return: ANSI escape code for moving cursor to the previous line.
@rtype: str
"""
return f"{lines}F"
@classmethod
def interact_cursor_at(cls, line, column):
"""
Moves the cursor to the specified position.
Abbr: CUP
@param line: Line number.
@type line: int
@param column: Column number.
@type column: int
@return: ANSI escape code for setting cursor position.
@rtype: str
"""
return f"{line};{column}H"
@classmethod
def interact_scroll_up(cls, lines=1):
"""
Scrolls the display up by the specified number of lines.
Abbr: SU
@param lines: Number of lines to scroll up. Defaults to 1.
@type lines: int
@return: ANSI escape code for scrolling up.
@rtype: str
"""
return f"{lines}S"
@classmethod
def interact_scroll_down(cls, lines=1):
"""
Scrolls the display down by the specified number of lines.
Abbr: SD
@param lines: Number of lines to scroll down. Defaults to 1.
@type lines: int
@return: ANSI escape code for scrolling down.
@rtype: str
"""
return f"{lines}T"
# endregion
# region templates
@classmethod
def success(cls, text):
"""
Formats text with a success style (green color).
@param text: Text to format.
@type text: str
@return: Formatted text.
@rtype: str
"""
return cls.style(text, cls.color_f_green)
@classmethod
def error(cls, text):
"""
Formats text with an error style (red color).
@param text: Text to format.
@type text: str
@return: Formatted text.
@rtype: str
"""
return cls.style(text, cls.color_f_red)
@classmethod
def warn(cls, text):
"""
Formats text with a warning style (yellow color).
@param text: Text to format.
@type text: str
@return: Formatted text.
@rtype: str
"""
return cls.style(text, cls.color_f_yellow)
@classmethod
def note(cls, text):
"""
Formats text with a note style (cyan color and bold).
@param text: Text to format.
@type text: str
@return: Formatted text.
@rtype: str
"""
return cls.style(text, cls.color_f_cyan, cls.format_bold)
@classmethod
def bold(cls, text):
"""
Formats text with a bold style.
@param text: Text to format.
@type text: str
@return: Formatted text.
@rtype: str
"""
return cls.style(text, cls.format_bold)
@classmethod
def underline(cls, text):
"""
Formats text with an underline style.
@param text: Text to format.
@type text: str
@return: Formatted text.
@rtype: str
"""
return cls.style(text, cls.format_underline)
@classmethod
def blink(cls, text):
"""
Formats text with a blink style.
@param text: Text to format.
@type text: str
@return: Formatted text.
@rtype: str
"""
return cls.style(text, cls.format_blink)
@classmethod
def cross(cls, text):
"""
Formats text with a strikethrough style.
@param text: Text to format.
@type text: str
@return: Formatted text.
@rtype: str
"""
return cls.style(text, cls.format_cross)
@classmethod
def box(cls, text):
"""
Formats text with a box style.
@param text: Text to format.
@type text: str
@return: Formatted text.
@rtype: str
"""
return cls.style(text, cls.format_box)
@classmethod
def clear_last_n_lines(cls, n: int, text=None):
"""
Clears the last n lines (and current line) on the terminal.
@param n: Number of lines to clear.
@type n: int
@param text: Optional text to display after clearing.
@type text: str
@return: ANSI escape code for clearing lines and optional text.
@rtype: str
"""
interactions = cls.clear_current_line()
for i in range(1, n + 1):
interactions += cls.interact(
cls.interact_cursor_prev_line(i),
cls.interact_clear_line,
)
return interactions + (text or "")
@classmethod
def clear_current_line(cls, text=None):
"""
Clears the current line on the terminal.
@param text: Optional text to display after clearing.
@type text: str
@return: ANSI escape code for clearing the current line and optional text.
@rtype: str
"""
# move the cursor to the beginning of the line and clear the line
return cls.interact(
cls.interact_cursor_prev_line(),
cls.interact_cursor_next_line(),
cls.interact_clear_line,
) + (text or "")
@classmethod
def clear_last_n_cells(cls, n, text=None):
"""
Clears the last n cells on the terminal line.
@param n: Number of cells to clear.
@type n: int
@param text: Optional text to display after clearing.
@type text: str
@return: ANSI escape code for clearing cells and optional text.
@rtype: str
"""
return cls.interact(
cls.interact_cursor_back(n),
cls.interact_clear_line_from_cursor,
) + (text or "")
@classmethod
def clear_screen(cls, text=None):
"""
Clears the terminal screen and buffer.
@param text: Optional text to display after clearing.
@type text: str
@return: ANSI escape code for clearing the screen and optional text.
@rtype: str
"""
return cls.interact(cls.interact_clear_screen_and_buffer) + (text or "")
# endregion
@classmethod
def style(cls, text, *styles):
"""
Applies the specified styles to the text.
@param text: Text to style.
@type text: str
@param styles: List of style codes to apply.
@return: Styled text.
@rtype: str
"""
if not styles:
return text
return f"{cls.tag}[{';'.join(styles)}m{text}{cls.tag}[0m"
@classmethod
def interact(cls, *interactions):
"""
Generates ANSI escape codes for the specified interactions.
@param interactions: List of interaction codes.
@return: ANSI escape codes.
@rtype: str
"""
if not interactions:
return ""
return f"{cls.tag}[" + f"{cls.tag}[".join(interactions)
def wrap_text(text, characters_per_line=130):
"""
Wraps the provided text to a specified number of characters per line.
This function takes a multi-line string and wraps each line to ensure
that no line exceeds the specified number of characters. It preserves
the indentation of each line.
Example
=======
>>> t = "This is a very long line that needs to be wrapped because it exceeds the maximum allowed characters per line."
>>> wrap_text(t, characters_per_line=20)
'This is a very long\\nline that needs to be\\nwrapped because it exceeds\\nthe maximum allowed\\ncharacters per line.'
@param text: The input text to be wrapped.
@type text: str
@param characters_per_line: The maximum number of characters per line. Defaults to 130.
@type characters_per_line: int
@return: The text with each line wrapped to the specified length.
@rtype: str
"""
def wrap_line(line, _add_lf=False):
"""
Wraps a single line to the specified number of characters.
@param line: The input line to be wrapped.
@type line: str
@param _add_lf: Used Internally to determine whether to add \n separator before wrapping. Defaults to False.
@type _add_lf: bool
@return: The wrapped line.
@rtype: str
"""
wrapped_line = ""
if _add_lf:
for character in line:
if character.isspace():
break
else:
wrapped_line += character
else:
# If the line ended and no spaces were found, return the line as is
return line
# Remove the processed part of the line (including the space)
line = line[len(wrapped_line) + 1 :]
wrapped_line += "\n"
for i, character in enumerate(line):
if i >= characters_per_line - 1:
# Recursively wrap the remaining part of the line
wrapped_line += wrap_line(line[i:], _add_lf=True)
break
else:
wrapped_line += character
return wrapped_line
output = []
for text_line in text.split("\n"):
prepend = ""
for ch in text_line:
if ch.isspace():
prepend += ch
else:
break
# Preserve the indentation and wrap the line
output.append(
prepend + f"\n{prepend}".join(wrap_line(text_line.strip()).split("\n"))
)
return "\n".join(output)
def execute(cmd, ask=False, timeout=0):
"""
Executes a shell command and optionally asks for confirmation.
Example
=======
>>> execute("echo hello")
'hello\\n'
>>> execute("sleep 2", timeout=1) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
TimeoutError: Command: sleep 2
@param cmd: The command to execute.
@type cmd: str
@param ask: If True, asks for confirmation before executing the command.
@type ask: bool
@param timeout: The maximum time (in seconds) to allow the command to run. If 0, no timeout is set.
@type timeout: int
@return: The output of the command.
@rtype: str
@raise TimeoutError: If the command times out.
"""
# Ask for confirmation before executing the command, if required
if ask and not confirm(None, FormatText.note(f"{cmd}\nExecute?")):
return ""
# Prepare the command with a timeout, if specified
if timeout:
_cmd = f'timeout {timeout} {cmd} 2>&1; [ $? -eq 124 ] && echo "BASH_TIMEOUT";'
else:
_cmd = f"{cmd} 2>&1"
# Execute the command and capture its output
with popen(_cmd) as f:
output = f.read()
# Check if the command output indicates a timeout
if output.endswith("BASH_TIMEOUT\n"):
raise TimeoutError(f"Command: {cmd}")
# Print the command output if confirmation was requested
if ask:
print(FormatText.note(output))
return output
def indent(text, level, spaces=False):
"""
Change the indentation level of the given text.
Example
=======
>>> indent('hello\\nhi', 1)
' hello\\n hi'
>>> indent('\\thello\\n\\thi', -2)
'hello\\nhi'
@param text: The text to be indented.
@type text: str
@param level: The number of levels to adjust the indentation by. Positive values increase indentation, negative values decrease it.
@type level: int
@param spaces: If True, use 4 spaces for indentation; otherwise, use tabs.
@type spaces: bool
@return: The text with adjusted indentation.
@rtype: str
"""
# Prepare the output list to accumulate the processed lines
output = []
# Determine the indentation character based on the 'spaces' parameter
ch = " " * 4 if spaces else "\t"
# Process each match from the regular expression to match lines with indentation
for match in re.finditer(r"^((?: {4}|\t)+)?(.*)$", text, flags=re.M):
group_indention, group_text = match.groups()
# Calculate the current indentation level
if group_indention is None:
curr_level = 0
else:
curr_level = group_indention.count(" ") + group_indention.count("\t")
# Calculate the new indentation level
new_level = curr_level + level
# Append the appropriately indented line to the output list
if new_level <= 0:
output.append(group_text)
else:
output.append(ch * new_level + group_text)
# Join the output list into a single string with newline characters
return "\n".join(output)
# endregion
# region Check
class Check:
"""
A class to represent a check with its associated rules, solutions, and status.
@cvar checks: A class attribute to keep track of all check instances.
@type checks: list[Check]
@cvar passed: A class attribute to keep track of passed check instances.
@type passed: list[Check]
@cvar failed: A class attribute to keep track of failed check instances.
@type failed: list[Check]
@cvar not_applicable: A class attribute to keep track of not applicable check instances.
@type not_applicable: list[Check]
@ivar id: The identifier for the check.
@type id: int
@ivar title: The title of the check.
@type title: str
@ivar description: The description of the check.
@type description: str
@ivar rationale: The rationale behind the check.
@type rationale: str
@ivar remediation: The remediation steps for the check.
@type remediation: str
@ivar compliance: The compliance information for the check.
@type compliance: list[dict[str, list[str]]]
@ivar references: The list of external references.
@type references: list[str]
@ivar condition: The condition to be met for the check.
@type condition: str
@ivar rules: The rules associated with the check.
@type rules: Rules
@ivar regex_type: The regex type to check against.
@type regex_type: str
@ivar solution: The solution associated with the check.
@type solution: Solution
@ivar status: The status of the check ("PASSED", "FAILED", "NOT_APPLICABLE").
@type status: str | None
"""
checks = []
passed = []
failed = []
not_applicable = []
def __init__(self, check):
"""
Initializes the Check object with the given check dictionary.
Check Model::
{
"id": 1,
"title": "Title",
"description": "Description",
"rationale": "Rationale",
"remediation": "Remediation",
"compliance": [{"key": ["value"]}],
"references": ["reference"],
"condition": "any or all or none",
"rules": ["Rule Regex"],
"regex_type": "RegexType",
"solution": SolutionModel,
}
@param check: The dictionary containing check information.
@type check: dict
"""
self.id = check["id"]
self.title = check["title"]
self.description = check.get("description", "")
self.rationale = check.get("rationale", "")
self.remediation = check.get("remediation", "")
self.compliance = check.get("compliance", [])
self.references = check.get("references", [])
self.condition = check["condition"]
self.rules = Rules(self.id, self.condition, check["rules"])
self.regex_type = check.get("regex_type") # todo
self.solution = Solution(self.id, check["solution"])
self.status = None
def __str__(self):
"""
Returns a string representation of the check.
@return: A string representation of the check.
@rtype: str
"""
# Format the check details into a string
txt = f"ID: {self.id}\n"
if self.title:
txt += f"Title:\n\t{self.title.strip()}\n"
if self.rationale:
txt += wrap_text(f"Rationale:\n\t{self.rationale.strip()}\n")
if self.remediation:
txt += wrap_text(f"Remediation:\n\t{self.remediation.strip()}\n")
if self.description:
txt += wrap_text(f"Description:\n\t{self.description.strip()}\n")
if self.regex_type:
txt += f"RegexType:\n\t{self.regex_type.strip()}\n"
# Add references information
if self.references:
txt += "References:\n\t- " + "\n\t- ".join(self.references) + "\n"
# Add rules information
txt += str(self.rules)
# Add compliance information
if self.compliance:
txt += "\nCompliance\n\t- " + "\n\t- ".join(
[
f"{k}: {','.join(v)}"
for item in self.compliance
for k, v in item.items()
]
)
# Add solution information
txt += "\n" + str(self.solution)
return txt
def __repr__(self):
"""
Returns a string representation of the check.
@return: A string representation of the check.
@rtype: str
"""
return self.__str__()
@classmethod
def check_all(cls):
"""
Class method to check all instances of Check.
Asks for confirmation before starting the checks.
Prints a summary report of passed, failed, and not applicable checks.
Offers to apply solutions for failed checks if available.
"""
if not cls.checks:
print(FormatText.error("No checks available"))
return
if not confirm("Checking", "Start Checks?"):
return
# Execute check for each instance of Check
for check in cls.checks:
check.check()
# Print summary report
available_solutions = [ins for ins in cls.failed if ins.solution.available]
print("\n\nCheck Report")
print(FormatText.success("[ PASSED ]"), len(cls.passed))
print(FormatText.error("[ FAILED ]"), len(cls.failed), FormatText.success(f"({len(available_solutions)} Solutions available)"))
print(
FormatText.style("[NOT APPLICABLE]", FormatText.color_f_black_bright),
len(cls.not_applicable),
)
print()
# Offer to apply solutions for failed checks
if available_solutions:
print(FormatText.success(f"{'=' * 32} Solutions {'=' * 32}"))
for item in available_solutions:
print("\t", item.id, item.title)
print()
if confirm(None, "Apply Available Solutions?"):
for solution in available_solutions:
solution.apply_solution()
@classmethod
def class_repr(cls):
"""Class method to print the representation of all checks."""
for check in cls.checks:
print(check)
@classmethod
def _load(cls, path):
"""
Class method to load or download yml configurations.
@param path: The path or url to the file containing configuration information.
@type path: str
"""
if path.startswith("http"):
# download
print(FormatText.success(f"Downloading configurations from {path}"))
return yaml_load(urllib.request.urlopen(path), YamlLoader)
else:
# load
p = Path(path)
if not p.exists():
raise FileNotFoundError(p)
if p.is_dir():
raise IsADirectoryError(p)
print(FormatText.success(f"Loading configurations from {path}"))
with open(p, "r") as f:
return yaml_load(f, YamlLoader)
@classmethod
def load(cls, cis, solutions=None, check_only=None):
"""
Class method to load checks from a file.
B{Note} If `solutions` is not given this function will try to load solutions from {cis}_solutions.yml
@param cis: The path or url to the file containing check information.
@type cis: str
@param solutions: The path or url to the file containing solutions information.
@type solutions: str | None
@param check_only: If specified only these checks will be loaded
@type check_only: list[int] | None
"""
print("Loading rules ...")
# region log the variables
if solutions:
print(FormatText.note(f"Solutions path: {solutions}"))
else:
print(
FormatText.note(
"Solutions path not specified. Will be detected automatically"
)
)
if check_only:
print(FormatText.note(f"Only Checking IDs: {check_only}"))
else:
print(
FormatText.note(
"No whitelisted checks specified. Will check all available Checks"
)
)
# endregion
# load cis
content = cls._load(cis)
# load solutions
try:
if solutions is None:
last_dot_index = cis.rfind(".")
solutions = cls._load(
cis[:last_dot_index] + "_solutions" + cis[last_dot_index:]
)
else:
solutions = cls._load(solutions)
except:
print(
FormatText.style(
"Error loading solutions",
FormatText.color_f_red,
FormatText.format_blink,
)
)
solutions = []
print(FormatText.note(f"{'=' * 32} {content['policy']['id']} {'=' * 32}"))
print(FormatText.note(content["policy"]["name"]))
print(FormatText.note(wrap_text(content["policy"]["description"])))
# check sca requirements
if not Rules(
0, content["requirements"]["condition"], content["requirements"]["rules"]
).check():
print(FormatText.error("Requirements not satisfied"))
exit()
for check in content["checks"]:
if check_only and check["id"] not in check_only:
continue
solution = check.get("solution")
if solution is None:
# No solution found within the main configuration. try to find solution from the loaded file
for item in solutions:
if item["id"] == check["id"]:
solution = item["solution"]
check["solution"] = solution
# Add the instance to the class-level checks list
cls.checks.append(cls(check))
print(FormatText.success("Loaded all rules"))
def check(self):
"""
Executes the check and updates the status.
@return: True if the check passes, False otherwise.
@rtype: bool
"""
res = self.rules.check()
if res:
self.status = "PASSED"
self.__class__.passed.append(self)
tag = FormatText.success(f"[ PASSED ]")
elif res is False:
self.status = "FAILED"
self.__class__.failed.append(self)
tag = FormatText.error(f"[ FAILED ]")
else:
self.status = "NOT_APPLICABLE"
self.__class__.not_applicable.append(self)
tag = FormatText.style(f"[NOT APPLICABLE]", FormatText.color_f_black_bright)
print(
tag,
self.solution.available and FormatText.success(f"[ SolutionAvailable ]") or FormatText.error(f"[ SolutionUnAvailable ]"),
self.id,
self.title,
)
return res
def apply_solution(self):
"""
Applies the solution associated with the check.
Attempts to apply the solution up to four times if necessary.
Asks for confirmation before each attempt and prints the progress.
"""
print("\n")
if not confirm(self.title, f"{self}\nApply?"):
return
for retry in range(self.solution.recheck and 5 or 1):
if retry == 4:
print(
FormatText.error(
"Reached Maximum tries to apply the solution. Moving on..."
)
)
return
if retry > 0: