From 309b6bec8479564489968a58ca8853c50c33ebbe Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Tue, 19 Mar 2024 20:52:27 +0000 Subject: [PATCH 1/3] black --- glitch/__main__.py | 170 ++- glitch/analysis/design.py | 165 ++- glitch/analysis/rules.py | 144 +-- glitch/analysis/security.py | 390 ++++--- glitch/analysis/terraform/__init__.py | 2 +- glitch/analysis/terraform/access_control.py | 222 +++- .../analysis/terraform/attached_resource.py | 26 +- glitch/analysis/terraform/authentication.py | 90 +- glitch/analysis/terraform/dns_policy.py | 44 +- .../analysis/terraform/firewall_misconfig.py | 60 +- glitch/analysis/terraform/http_without_tls.py | 50 +- glitch/analysis/terraform/integrity_policy.py | 42 +- glitch/analysis/terraform/key_management.py | 118 ++- glitch/analysis/terraform/logging.py | 529 +++++++--- .../analysis/terraform/missing_encryption.py | 196 +++- glitch/analysis/terraform/naming.py | 130 ++- glitch/analysis/terraform/network_policy.py | 164 ++- .../terraform/permission_iam_policies.py | 63 +- glitch/analysis/terraform/public_ip.py | 55 +- glitch/analysis/terraform/replication.py | 67 +- .../terraform/sensitive_iam_action.py | 100 +- glitch/analysis/terraform/smell_checker.py | 129 ++- glitch/analysis/terraform/ssl_tls_policy.py | 71 +- .../analysis/terraform/threats_detection.py | 77 +- glitch/analysis/terraform/versioning.py | 43 +- .../terraform/weak_password_key_policy.py | 100 +- glitch/configs/default.ini | 1 - glitch/exceptions.py | 3 +- glitch/helpers.py | 111 +- glitch/parsers/ansible.py | 198 ++-- glitch/parsers/chef.py | 423 +++++--- glitch/parsers/docker.py | 211 ++-- glitch/parsers/parser.py | 3 +- glitch/parsers/puppet.py | 436 +++++--- glitch/parsers/ripper_parser.py | 92 +- glitch/parsers/terraform.py | 150 ++- .../repair/interactive/compiler/compiler.py | 43 +- glitch/repair/interactive/compiler/labeler.py | 46 +- .../interactive/compiler/names_database.py | 20 +- glitch/repair/interactive/delta_p.py | 4 +- glitch/repair/interactive/solver.py | 28 +- glitch/repair/interactive/tracer/tracer.py | 1 - glitch/repair/interactive/values.py | 1 + glitch/repr/inter.py | 133 ++- glitch/stats/print.py | 93 +- glitch/stats/stats.py | 4 +- glitch/tests/design/ansible/test_design.py | 72 +- glitch/tests/design/chef/test_design.py | 74 +- glitch/tests/design/docker/test_design.py | 23 +- glitch/tests/design/puppet/test_design.py | 79 +- glitch/tests/design/terraform/test_design.py | 48 +- glitch/tests/hierarchical/test_parsers.py | 17 +- glitch/tests/parser/puppet/test_parser.py | 9 +- glitch/tests/parser/terraform/test_parser.py | 39 +- .../tests/repair/interactive/test_delta_p.py | 5 +- .../repair/interactive/test_patch_solver.py | 26 +- .../interactive/test_tracer_transform.py | 6 +- .../tests/security/ansible/test_security.py | 51 +- glitch/tests/security/chef/test_security.py | 52 +- glitch/tests/security/docker/test_security.py | 45 +- glitch/tests/security/puppet/test_security.py | 51 +- .../tests/security/terraform/test_security.py | 987 +++++++++++++----- 62 files changed, 4685 insertions(+), 2147 deletions(-) diff --git a/glitch/__main__.py b/glitch/__main__.py index 76e46124..6ef17617 100644 --- a/glitch/__main__.py +++ b/glitch/__main__.py @@ -16,9 +16,10 @@ # NOTE: These are necessary in order for python to load the visitors. # Otherwise, python will not consider these types of rules. -from glitch.analysis.design import DesignVisitor +from glitch.analysis.design import DesignVisitor from glitch.analysis.security import SecurityVisitor + def parse_and_check(type, path, module, parser, analyses, errors, stats): inter = parser.parse(path, type, module) if inter != None: @@ -26,45 +27,99 @@ def parse_and_check(type, path, module, parser, analyses, errors, stats): errors += analysis.check(inter) stats.compute(inter) + @click.command( help="PATH is the file or folder to analyze. OUTPUT is an optional file to which we can redirect the smells output." ) -@click.option('--tech', - type=click.Choice(Tech), required=True, - help="The IaC technology in which the scripts analyzed are written in.") -@click.option('--tableformat', - type=click.Choice(("prettytable", "latex")), required=False, default="prettytable", - help="The presentation format of the tables that show stats about the run.") -@click.option('--type', - type=click.Choice(UnitBlockType), default=UnitBlockType.unknown, - help="The type of scripts being analyzed.") -@click.option('--config', type=click.Path(), default="configs/default.ini", - help="The path for a config file. Otherwise the default config will be used.") -@click.option('--module', is_flag=True, default=False, - help="Use this flag if the folder you are going to analyze is a module (e.g. Chef cookbook).") -@click.option('--dataset', is_flag=True, default=False, - help="Use this flag if the folder being analyzed is a dataset. A dataset is a folder with subfolders to be analyzed.") -@click.option('--linter', is_flag=True, default=False, - help="This flag changes the output to be more usable for other interfaces, such as, extensions for code editors.") -@click.option('--includeall', multiple=True, - help="Some files are ignored when analyzing a folder. For instance, sometimes only some" - "folders in the folder structure are considered. Use this option if" - "you want to analyze all the files with a certain extension inside a folder. (e.g. --includeall yml)" - "This flag is only relevant if you are using the dataset flag.") -@click.option('--csv', is_flag=True, default=False, - help="Use this flag if you want the output to be in CSV format.") -@click.option('--smell_types', cls=RulesListOption, multiple=True, - help="The type of smell_types being analyzed.") -@click.argument('path', type=click.Path(exists=True), required=True) -@click.argument('output', type=click.Path(), required=False) -def glitch(tech, type, path, config, module, csv, - dataset, includeall, smell_types, output, tableformat, linter): +@click.option( + "--tech", + type=click.Choice(Tech), + required=True, + help="The IaC technology in which the scripts analyzed are written in.", +) +@click.option( + "--tableformat", + type=click.Choice(("prettytable", "latex")), + required=False, + default="prettytable", + help="The presentation format of the tables that show stats about the run.", +) +@click.option( + "--type", + type=click.Choice(UnitBlockType), + default=UnitBlockType.unknown, + help="The type of scripts being analyzed.", +) +@click.option( + "--config", + type=click.Path(), + default="configs/default.ini", + help="The path for a config file. Otherwise the default config will be used.", +) +@click.option( + "--module", + is_flag=True, + default=False, + help="Use this flag if the folder you are going to analyze is a module (e.g. Chef cookbook).", +) +@click.option( + "--dataset", + is_flag=True, + default=False, + help="Use this flag if the folder being analyzed is a dataset. A dataset is a folder with subfolders to be analyzed.", +) +@click.option( + "--linter", + is_flag=True, + default=False, + help="This flag changes the output to be more usable for other interfaces, such as, extensions for code editors.", +) +@click.option( + "--includeall", + multiple=True, + help="Some files are ignored when analyzing a folder. For instance, sometimes only some" + "folders in the folder structure are considered. Use this option if" + "you want to analyze all the files with a certain extension inside a folder. (e.g. --includeall yml)" + "This flag is only relevant if you are using the dataset flag.", +) +@click.option( + "--csv", + is_flag=True, + default=False, + help="Use this flag if you want the output to be in CSV format.", +) +@click.option( + "--smell_types", + cls=RulesListOption, + multiple=True, + help="The type of smell_types being analyzed.", +) +@click.argument("path", type=click.Path(exists=True), required=True) +@click.argument("output", type=click.Path(), required=False) +def glitch( + tech, + type, + path, + config, + module, + csv, + dataset, + includeall, + smell_types, + output, + tableformat, + linter, +): if config != "configs/default.ini" and not os.path.exists(config): - raise click.BadOptionUsage('config', f"Invalid value for 'config': Path '{config}' does not exist.") + raise click.BadOptionUsage( + "config", f"Invalid value for 'config': Path '{config}' does not exist." + ) elif os.path.isdir(config): - raise click.BadOptionUsage('config', f"Invalid value for 'config': Path '{config}' should be a file.") + raise click.BadOptionUsage( + "config", f"Invalid value for 'config': Path '{config}' should be a file." + ) elif config == "configs/default.ini": - config = resource_filename('glitch', "configs/default.ini") + config = resource_filename("glitch", "configs/default.ini") parser = None if tech == Tech.ansible: @@ -77,7 +132,7 @@ def glitch(tech, type, path, config, module, csv, parser = DockerParser() elif tech == Tech.terraform: parser = TerraformParser() - config = resource_filename('glitch', "configs/terraform.ini") + config = resource_filename("glitch", "configs/terraform.ini") file_stats = FileStats() if smell_types == (): @@ -97,35 +152,45 @@ def glitch(tech, type, path, config, module, csv, iac_files = [] for root, _, files in os.walk(path): for name in files: - name_split = name.split('.') - if name_split[-1] in includeall \ - and not Path(os.path.join(root, name)).is_symlink(): + name_split = name.split(".") + if ( + name_split[-1] in includeall + and not Path(os.path.join(root, name)).is_symlink() + ): iac_files.append(os.path.join(root, name)) iac_files = set(iac_files) - with alive_bar(len(iac_files), - title=f"ANALYZING ALL FILES WITH EXTENSIONS {includeall}") as bar: + with alive_bar( + len(iac_files), + title=f"ANALYZING ALL FILES WITH EXTENSIONS {includeall}", + ) as bar: for file in iac_files: - parse_and_check(type, file, module, parser, analyses, errors, file_stats) + parse_and_check( + type, file, module, parser, analyses, errors, file_stats + ) bar() else: subfolders = [f.path for f in os.scandir(f"{path}") if f.is_dir()] with alive_bar(len(subfolders), title="ANALYZING SUBFOLDERS") as bar: for d in subfolders: - parse_and_check(type, d, module, parser, analyses, errors, file_stats) + parse_and_check( + type, d, module, parser, analyses, errors, file_stats + ) bar() files = [f.path for f in os.scandir(f"{path}") if f.is_file()] with alive_bar(len(files), title="ANALYZING FILES IN ROOT FOLDER") as bar: for file in files: - parse_and_check(type, file, module, parser, analyses, errors, file_stats) + parse_and_check( + type, file, module, parser, analyses, errors, file_stats + ) bar() - else: + else: parse_and_check(type, path, module, parser, analyses, errors, file_stats) - + errors = sorted(set(errors), key=lambda e: (e.path, e.line, e.code)) - + if output is None: f = sys.stdout else: @@ -133,19 +198,22 @@ def glitch(tech, type, path, config, module, csv, if linter: for error in errors: - print(Error.ALL_ERRORS[error.code] + "," + error.to_csv(), file = f) + print(Error.ALL_ERRORS[error.code] + "," + error.to_csv(), file=f) elif csv: for error in errors: - print(error.to_csv(), file = f) + print(error.to_csv(), file=f) else: for error in errors: - print(error, file = f) + print(error, file=f) - if f != sys.stdout: f.close() + if f != sys.stdout: + f.close() if not linter: print_stats(errors, get_smells(smell_types, tech), file_stats, tableformat) + def main(): - glitch(prog_name='glitch') + glitch(prog_name="glitch") + main() diff --git a/glitch/analysis/design.py b/glitch/analysis/design.py index db1be7c8..b20dd388 100644 --- a/glitch/analysis/design.py +++ b/glitch/analysis/design.py @@ -7,6 +7,7 @@ from glitch.repr.inter import * + class DesignVisitor(RuleVisitor): class ImproperAlignmentSmell(SmellChecker): def check(self, element, file: str): @@ -16,11 +17,17 @@ def check(self, element, file: str): first_line = a.code.split("\n")[0] curr_id = len(first_line) - len(first_line.lstrip()) - if (identation is None): + if identation is None: identation = curr_id - elif (identation != curr_id): - return [Error('implementation_improper_alignment', - element, file, repr(element))] + elif identation != curr_id: + return [ + Error( + "implementation_improper_alignment", + element, + file, + repr(element), + ) + ] return [] return [] @@ -40,25 +47,38 @@ def check(self, element, file: str) -> list[Error]: longest_ident = 0 longest_split = "" for a in element.attributes: - if len(a.name) > longest and '=>' in a.code: + if len(a.name) > longest and "=>" in a.code: longest = len(a.name) - split = lines[a.line - 1].split('=>')[0] + split = lines[a.line - 1].split("=>")[0] longest_ident = len(split) longest_split = split - if longest_split == "": return [] + if longest_split == "": + return [] elif len(longest_split) - 1 != len(longest_split.rstrip()): - return [Error('implementation_improper_alignment', - element, file, repr(element))] + return [ + Error( + "implementation_improper_alignment", + element, + file, + repr(element), + ) + ] for a in element.attributes: first_line = lines[a.line - 1] - cur_arrow_column = len(first_line.split('=>')[0]) + cur_arrow_column = len(first_line.split("=>")[0]) if cur_arrow_column != longest_ident: - return [Error('implementation_improper_alignment', - element, file, repr(element))] + return [ + Error( + "implementation_improper_alignment", + element, + file, + repr(element), + ) + ] return [] - + class AnsibleImproperAlignmentSmell(SmellChecker): # YAML does not allow improper alignments (it also would have problems with generic attributes for all modules) def check(self, element: AtomicUnit, file: str): @@ -83,7 +103,11 @@ def check(self, element, file: str): order.append(4) if order != sorted(order): - return [Error('design_misplaced_attribute', element, file, repr(element))] + return [ + Error( + "design_misplaced_attribute", element, file, repr(element) + ) + ] return [] class PuppetMisplacedAttribute(SmellChecker): @@ -91,14 +115,28 @@ def check(self, element, file: str): if isinstance(element, AtomicUnit): for i, attr in enumerate(element.attributes): if attr.name == "ensure" and i != 0: - return [Error('design_misplaced_attribute', element, file, repr(element))] + return [ + Error( + "design_misplaced_attribute", + element, + file, + repr(element), + ) + ] elif isinstance(element, UnitBlock): optional = False for attr in element.attributes: if attr.value is not None: optional = True elif optional == True: - return [Error('design_misplaced_attribute', element, file, repr(element))] + return [ + Error( + "design_misplaced_attribute", + element, + file, + repr(element), + ) + ] return [] def __init__(self, tech: Tech) -> None: @@ -134,12 +172,16 @@ def get_name() -> str: def config(self, config_path: str): config = configparser.ConfigParser() config.read(config_path) - DesignVisitor.__EXEC = json.loads(config['design']['exec_atomic_units']) - DesignVisitor.__DEFAULT_VARIABLES = json.loads(config['design']['default_variables']) - if 'var_refer_symbol' not in config['design']: + DesignVisitor.__EXEC = json.loads(config["design"]["exec_atomic_units"]) + DesignVisitor.__DEFAULT_VARIABLES = json.loads( + config["design"]["default_variables"] + ) + if "var_refer_symbol" not in config["design"]: DesignVisitor.__VAR_REFER_SYMBOL = None else: - DesignVisitor.__VAR_REFER_SYMBOL = json.loads(config['design']['var_refer_symbol']) + DesignVisitor.__VAR_REFER_SYMBOL = json.loads( + config["design"]["var_refer_symbol"] + ) def check_module(self, m: Module) -> list[Error]: errors = super().check_module(m) @@ -155,7 +197,7 @@ def count_atomic_units(ub: UnitBlock): for au in ub.atomic_units: if au.type in DesignVisitor.__EXEC: count_execs += 1 - + for unitblock in ub.unit_blocks: resources, execs = count_atomic_units(unitblock) count_resources += resources @@ -174,13 +216,13 @@ def count_atomic_units(ub: UnitBlock): self.first_non_comm_line = inf for i, line in enumerate(code_lines): - if not line.startswith(self.comment): + if not line.startswith(self.comment): self.first_non_comm_line = i + 1 - break + break self.variable_stack.append(len(self.variables_names)) for attr in u.attributes: - self.variables_names.append(attr.name) + self.variables_names.append(attr.name) errors = [] # The order is important @@ -200,16 +242,15 @@ def count_atomic_units(ub: UnitBlock): total_resources, total_execs = count_atomic_units(u) if total_execs > 2 and (total_execs / total_resources) > 0.20: - errors.append(Error('design_imperative_abstraction', u, u.path, repr(u))) + errors.append(Error("design_imperative_abstraction", u, u.path, repr(u))) for i, line in enumerate(code_lines): - if ("\t" in line): - error = Error('implementation_improper_alignment', - u, u.path, repr(u)) + if "\t" in line: + error = Error("implementation_improper_alignment", u, u.path, repr(u)) error.line = i + 1 errors.append(error) if len(line) > 140: - error = Error('implementation_long_statement', u, u.path, line) + error = Error("implementation_long_statement", u, u.path, line) error.line = i + 1 errors.append(error) @@ -221,24 +262,38 @@ def count_variables(vars: list[Variable]): else: count += 1 return count - + # The UnitBlock should not be of type vars, because these files are supposed to only # have variables - if count_variables(u.variables) / max(len(code_lines), 1) > 0.3 and u.type != UnitBlockType.vars: - errors.append(Error('implementation_too_many_variables', u, u.path, repr(u))) + if ( + count_variables(u.variables) / max(len(code_lines), 1) > 0.3 + and u.type != UnitBlockType.vars + ): + errors.append( + Error("implementation_too_many_variables", u, u.path, repr(u)) + ) if DesignVisitor.__VAR_REFER_SYMBOL is not None: # FIXME could be improved if we considered strings as part of the model for i, l in enumerate(code_lines): - for tuple in re.findall(r'(\'([^\\]|(\\(\n|.)))*?\')|(\"([^\\]|(\\(\n|.)))*?\")', l): + for tuple in re.findall( + r"(\'([^\\]|(\\(\n|.)))*?\')|(\"([^\\]|(\\(\n|.)))*?\")", l + ): for string in (tuple[0], tuple[4]): - for var in self.variables_names + DesignVisitor.__DEFAULT_VARIABLES: + for var in ( + self.variables_names + DesignVisitor.__DEFAULT_VARIABLES + ): if (DesignVisitor.__VAR_REFER_SYMBOL + var) in string[1:-1]: - error = Error('implementation_unguarded_variable', u, u.path, string) + error = Error( + "implementation_unguarded_variable", + u, + u.path, + string, + ) error.line = i + 1 errors.append(error) - def get_line(i ,lines): + def get_line(i, lines): for j, line in lines: if i < j: return line @@ -247,7 +302,7 @@ def get_line(i ,lines): current_line = 1 i = 0 for c in all_code: - if c == '\n': + if c == "\n": lines.append((i, current_line)) current_line += 1 elif not c.isspace(): @@ -270,7 +325,9 @@ def get_line(i ,lines): for i in value: if i not in checked: line = get_line(i, lines) - error = Error('design_duplicate_block', u, u.path, code_lines[line - 1]) + error = Error( + "design_duplicate_block", u, u.path, code_lines[line - 1] + ) error.line = line errors.append(error) checked.update(range(i, i + 150)) @@ -290,8 +347,10 @@ def get_line(i ,lines): errors += self.check_unitblock(ub) variable_size = self.variable_stack.pop() - if (variable_size == 0): self.variables_names = [] - else: self.variables_names = self.variables_names[:variable_size] + if variable_size == 0: + self.variables_names = [] + else: + self.variables_names = self.variables_names[:variable_size] return errors @@ -304,23 +363,27 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: errors += self.misplaced_attr.check(au, file) if au.type in DesignVisitor.__EXEC: - if ("&&" in au.name or ";" in au.name or "|" in au.name): - errors.append(Error("design_multifaceted_abstraction", au, file, repr(au))) + if "&&" in au.name or ";" in au.name or "|" in au.name: + errors.append( + Error("design_multifaceted_abstraction", au, file, repr(au)) + ) else: for attribute in au.attributes: value = repr(attribute.value) - if ("&&" in value or ";" in value or "|" in value): - errors.append(Error("design_multifaceted_abstraction", au, file, repr(au))) + if "&&" in value or ";" in value or "|" in value: + errors.append( + Error("design_multifaceted_abstraction", au, file, repr(au)) + ) break - if au.type in DesignVisitor.__EXEC: lines = 0 for attr in au.attributes: - for line in attr.code.split('\n'): - if line.strip() != "": lines += 1 + for line in attr.code.split("\n"): + if line.strip() != "": + lines += 1 - if lines > 7: + if lines > 7: errors.append(Error("design_long_resource", au, file, repr(au))) return errors @@ -328,7 +391,9 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> list[Error]: def check_dependency(self, d: Dependency, file: str) -> list[Error]: return [] - def check_attribute(self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "") -> list[Error]: + def check_attribute( + self, a: Attribute, file: str, au: AtomicUnit = None, parent_name: str = "" + ) -> list[Error]: return [] def check_variable(self, v: Variable, file: str) -> list[Error]: @@ -338,5 +403,5 @@ def check_variable(self, v: Variable, file: str) -> list[Error]: def check_comment(self, c: Comment, file: str) -> list[Error]: errors = [] if c.line >= self.first_non_comm_line: - errors.append(Error('design_avoid_comments', c, file, repr(c))) + errors.append(Error("design_avoid_comments", c, file, repr(c))) return errors diff --git a/glitch/analysis/rules.py b/glitch/analysis/rules.py index 09abe641..49e5ba33 100644 --- a/glitch/analysis/rules.py +++ b/glitch/analysis/rules.py @@ -2,60 +2,61 @@ from glitch.repr.inter import * from abc import ABC, abstractmethod -class Error(): + +class Error: ERRORS = { - 'security': { - 'sec_https': "Use of HTTP without TLS - The developers should always favor the usage of HTTPS. (CWE-319)", - 'sec_susp_comm': "Suspicious comment - Comments with keywords such as TODO HACK or FIXME may reveal problems possibly exploitable. (CWE-546)", - 'sec_def_admin': "Admin by default - Developers should always try to give the least privileges possible. Admin privileges may indicate a security problem. (CWE-250)", - 'sec_empty_pass': "Empty password - An empty password is indicative of a weak password which may lead to a security breach. (CWE-258)", - 'sec_weak_crypt': "Weak Crypto Algorithm - Weak crypto algorithms should be avoided since they are susceptible to security issues. (CWE-326 | CWE-327)", - 'sec_hard_secr': "Hard-coded secret - Developers should not reveal sensitive information in the source code. (CWE-798)", - 'sec_hard_pass': "Hard-coded password - Developers should not reveal sensitive information in the source code. (CWE-259)", - 'sec_hard_user': "Hard-coded user - Developers should not reveal sensitive information in the source code. (CWE-798)", - 'sec_invalid_bind': "Invalid IP address binding - Binding to the address 0.0.0.0 allows connections from every possible network which might be a security issues. (CWE-284)", - 'sec_no_int_check': "No integrity check - The content of files downloaded from the internet should be checked. (CWE-353)", - 'sec_no_default_switch': "Missing default case statement - Not handling every possible input combination might allow an attacker to trigger an error for an unhandled value. (CWE-478)", - 'sec_full_permission_filesystem': "Full permission to the filesystem - Files should not have full permissions to every user. (CWE-732)", - 'sec_obsolete_command': "Use of obsolete command or function - Avoid using obsolete or deprecated commands and functions. (CWE-477)", + "security": { + "sec_https": "Use of HTTP without TLS - The developers should always favor the usage of HTTPS. (CWE-319)", + "sec_susp_comm": "Suspicious comment - Comments with keywords such as TODO HACK or FIXME may reveal problems possibly exploitable. (CWE-546)", + "sec_def_admin": "Admin by default - Developers should always try to give the least privileges possible. Admin privileges may indicate a security problem. (CWE-250)", + "sec_empty_pass": "Empty password - An empty password is indicative of a weak password which may lead to a security breach. (CWE-258)", + "sec_weak_crypt": "Weak Crypto Algorithm - Weak crypto algorithms should be avoided since they are susceptible to security issues. (CWE-326 | CWE-327)", + "sec_hard_secr": "Hard-coded secret - Developers should not reveal sensitive information in the source code. (CWE-798)", + "sec_hard_pass": "Hard-coded password - Developers should not reveal sensitive information in the source code. (CWE-259)", + "sec_hard_user": "Hard-coded user - Developers should not reveal sensitive information in the source code. (CWE-798)", + "sec_invalid_bind": "Invalid IP address binding - Binding to the address 0.0.0.0 allows connections from every possible network which might be a security issues. (CWE-284)", + "sec_no_int_check": "No integrity check - The content of files downloaded from the internet should be checked. (CWE-353)", + "sec_no_default_switch": "Missing default case statement - Not handling every possible input combination might allow an attacker to trigger an error for an unhandled value. (CWE-478)", + "sec_full_permission_filesystem": "Full permission to the filesystem - Files should not have full permissions to every user. (CWE-732)", + "sec_obsolete_command": "Use of obsolete command or function - Avoid using obsolete or deprecated commands and functions. (CWE-477)", Tech.docker: { - 'sec_non_official_image': "Use of non-official Docker image - Use of non-official images should be avoided or taken into careful consideration. (CWE-829)", + "sec_non_official_image": "Use of non-official Docker image - Use of non-official images should be avoided or taken into careful consideration. (CWE-829)", }, Tech.terraform: { - 'sec_integrity_policy': "Integrity Policy - Image tag is prone to be mutable or integrity monitoring is disabled. (CWE-471)", - 'sec_ssl_tls_policy': "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions. (CWE-326)", - 'sec_dnssec': "Use of DNS without DNSSEC - Developers should favor the usage of DNSSEC while using DNS. (CWE-350)", - 'sec_public_ip': "Associated Public IP address - Associating Public IP addresses allows connections from public internet. (CWE-1327)", - 'sec_access_control': "Insecure Access Control - Developers should be aware of possible unauthorized access. (CWE-284)", - 'sec_authentication': "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled. (CWE-287 | CWE-306)", - 'sec_missing_encryption': "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)", - 'sec_firewall_misconfig': "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)", - 'sec_threats_detection_alerts': "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible. (CWE-693)", - 'sec_weak_password_key_policy': "Weak Password/Key Policy - Developers should favor the usage of strong password/key requirements and configurations. (CWE-521).", - 'sec_sensitive_iam_action': "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies. (CWE-284)", - 'sec_key_management': "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption. (CWE-1394)", - 'sec_network_security_rules': "Network Security Rules - Developers should enforce that only secure network rules are being used. (CWE-923)", - 'sec_permission_iam_policies': "Permission of IAM Policies - Developers should be aware of unwanted permissions of IAM policies. (CWE-732 | CWE-284)", - 'sec_logging': "Logging - Logs should be enabled and securely configured to help monitoring and preventing security problems. (CWE-223 | CWE-778)", - 'sec_attached_resource': "Attached Resource - Ensure that Route53 A records point to resources part of your account rather than just random IP addresses. (CWE-200)", - 'sec_versioning': "Versioning - Ensure that versioning is enabled so that users can retrieve and restore previous versions.", - 'sec_naming': "Naming - Ensure storage accounts adhere to the naming rules and every security groups and rules have a description. (CWE-1099 | CWE-710)", - 'sec_replication': "Replication - Ensure that cross-region replication is enabled to allow copying objects across S3 buckets.", - } + "sec_integrity_policy": "Integrity Policy - Image tag is prone to be mutable or integrity monitoring is disabled. (CWE-471)", + "sec_ssl_tls_policy": "SSL/TLS/mTLS Policy - Developers should use SSL/TLS/mTLS protocols and their secure versions. (CWE-326)", + "sec_dnssec": "Use of DNS without DNSSEC - Developers should favor the usage of DNSSEC while using DNS. (CWE-350)", + "sec_public_ip": "Associated Public IP address - Associating Public IP addresses allows connections from public internet. (CWE-1327)", + "sec_access_control": "Insecure Access Control - Developers should be aware of possible unauthorized access. (CWE-284)", + "sec_authentication": "Disabled/Weak Authentication - Developers should guarantee that authentication is enabled. (CWE-287 | CWE-306)", + "sec_missing_encryption": "Missing Encryption - Developers should ensure encryption of sensitive and critical data. (CWE-311)", + "sec_firewall_misconfig": "Firewall Misconfiguration - Developers should favor the usage of a well configured waf. (CWE-693)", + "sec_threats_detection_alerts": "Missing Threats Detection/Alerts - Developers should enable threats detection and alerts when it is possible. (CWE-693)", + "sec_weak_password_key_policy": "Weak Password/Key Policy - Developers should favor the usage of strong password/key requirements and configurations. (CWE-521).", + "sec_sensitive_iam_action": "Sensitive Action by IAM - Developers should use the principle of least privilege when defining IAM policies. (CWE-284)", + "sec_key_management": "Key Management - Developers should use well configured Customer Managed Keys (CMK) for encryption. (CWE-1394)", + "sec_network_security_rules": "Network Security Rules - Developers should enforce that only secure network rules are being used. (CWE-923)", + "sec_permission_iam_policies": "Permission of IAM Policies - Developers should be aware of unwanted permissions of IAM policies. (CWE-732 | CWE-284)", + "sec_logging": "Logging - Logs should be enabled and securely configured to help monitoring and preventing security problems. (CWE-223 | CWE-778)", + "sec_attached_resource": "Attached Resource - Ensure that Route53 A records point to resources part of your account rather than just random IP addresses. (CWE-200)", + "sec_versioning": "Versioning - Ensure that versioning is enabled so that users can retrieve and restore previous versions.", + "sec_naming": "Naming - Ensure storage accounts adhere to the naming rules and every security groups and rules have a description. (CWE-1099 | CWE-710)", + "sec_replication": "Replication - Ensure that cross-region replication is enabled to allow copying objects across S3 buckets.", + }, + }, + "design": { + "design_imperative_abstraction": "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", + "design_unnecessary_abstraction": "Unnecessary abstraction - Blocks should contain declarations or statements, otherwise they are unnecessary.", + "implementation_long_statement": "Long statement - Long statements may decrease the readability and maintainability of the code.", + "implementation_improper_alignment": "Improper alignment - The developers should try to follow the languages' style guides. These style guides define how the attributes in an atomic unit should be aligned. The developers should also avoid the use of tabs.", + "implementation_too_many_variables": "Too many variables - The existence of too many variables in a single IaC script may reveal that the script is being used for too many purposes.", + "design_duplicate_block": "Duplicate block - Duplicates blocks may reveal a missing abstraction.", + "implementation_unguarded_variable": "Unguarded variable - Variables should be guarded for readability and maintainability of the code.", + "design_avoid_comments": "Avoid comments - Comments may lead to bad code or be used as a way to justify bad code.", + "design_long_resource": "Long Resource - Long resources may decrease the readability and maintainability of the code.", + "design_multifaceted_abstraction": "Multifaceted Abstraction - Each block should only specify the properties of a single piece of software.", + "design_misplaced_attribute": "Misplaced attribute - The developers should try to follow the languages' style guides. These style guides define the expected attribute order.", }, - 'design': { - 'design_imperative_abstraction': "Imperative abstraction - The presence of imperative statements defies the purpose of IaC declarative languages.", - 'design_unnecessary_abstraction': "Unnecessary abstraction - Blocks should contain declarations or statements, otherwise they are unnecessary.", - 'implementation_long_statement': "Long statement - Long statements may decrease the readability and maintainability of the code.", - 'implementation_improper_alignment': "Improper alignment - The developers should try to follow the languages' style guides. These style guides define how the attributes in an atomic unit should be aligned. The developers should also avoid the use of tabs.", - 'implementation_too_many_variables': "Too many variables - The existence of too many variables in a single IaC script may reveal that the script is being used for too many purposes.", - 'design_duplicate_block': "Duplicate block - Duplicates blocks may reveal a missing abstraction.", - 'implementation_unguarded_variable': "Unguarded variable - Variables should be guarded for readability and maintainability of the code.", - 'design_avoid_comments': "Avoid comments - Comments may lead to bad code or be used as a way to justify bad code.", - 'design_long_resource': "Long Resource - Long resources may decrease the readability and maintainability of the code.", - 'design_multifaceted_abstraction': "Multifaceted Abstraction - Each block should only specify the properties of a single piece of software.", - 'design_misplaced_attribute': "Misplaced attribute - The developers should try to follow the languages' style guides. These style guides define the expected attribute order." - } } ALL_ERRORS = {} @@ -68,9 +69,12 @@ def aux_agglomerate_errors(key, errors): aux_agglomerate_errors(k, v) else: Error.ALL_ERRORS[key] = errors - aux_agglomerate_errors('', Error.ERRORS) - def __init__(self, code: str, el, path: str, repr: str, opt_msg: str = None) -> None: + aux_agglomerate_errors("", Error.ERRORS) + + def __init__( + self, code: str, el, path: str, repr: str, opt_msg: str = None + ) -> None: self.code: str = code self.el = el self.path = path @@ -83,7 +87,7 @@ def __init__(self, code: str, el, path: str, repr: str, opt_msg: str = None) -> self.line = -1 def to_csv(self) -> str: - repr = self.repr.split('\n')[0].strip() + repr = self.repr.split("\n")[0].strip() if self.opt_msg: return f"{self.path},{self.line},{self.code},{repr},{self.opt_msg}" else: @@ -91,23 +95,34 @@ def to_csv(self) -> str: def __repr__(self) -> str: with open(self.path) as f: - line = f.readlines()[self.line - 1].strip() if self.line != -1 else self.repr.split('\n')[0] + line = ( + f.readlines()[self.line - 1].strip() + if self.line != -1 + else self.repr.split("\n")[0] + ) if self.opt_msg: line += f"\n-> {self.opt_msg}" - return \ - f"{self.path}\nIssue on line {self.line}: {Error.ALL_ERRORS[self.code]}\n" + \ - f"{line}\n" + return ( + f"{self.path}\nIssue on line {self.line}: {Error.ALL_ERRORS[self.code]}\n" + + f"{line}\n" + ) def __hash__(self): return hash((self.code, self.path, self.line, self.opt_msg)) def __eq__(self, other): - if not isinstance(other, type(self)): return NotImplemented - return self.code == other.code and self.path == other.path and\ - self.line == other.line + if not isinstance(other, type(self)): + return NotImplemented + return ( + self.code == other.code + and self.path == other.path + and self.line == other.line + ) + Error.agglomerate_errors() + class RuleVisitor(ABC): def __init__(self, tech: Tech) -> None: super().__init__() @@ -123,7 +138,9 @@ def check(self, code) -> list[Error]: elif isinstance(code, UnitBlock): return self.check_unitblock(code) - def check_element(self, c, file: str, au_type = None, parent_name: str = "") -> list[Error]: + def check_element( + self, c, file: str, au_type=None, parent_name: str = "" + ) -> list[Error]: if isinstance(c, AtomicUnit): return self.check_atomicunit(c, file) elif isinstance(c, Dependency): @@ -201,7 +218,9 @@ def check_dependency(self, d: Dependency, file: str) -> list[Error]: pass @abstractmethod - def check_attribute(self, a: Attribute, file: str, au_type: None, parent_name: str = "") -> list[Error]: + def check_attribute( + self, a: Attribute, file: str, au_type: None, parent_name: str = "" + ) -> list[Error]: pass @abstractmethod @@ -219,8 +238,11 @@ def check_condition(self, c: ConditionalStatement, file: str) -> list[Error]: @abstractmethod def check_comment(self, c: Comment, file: str) -> list[Error]: pass + + Error.agglomerate_errors() + class SmellChecker(ABC): def __init__(self) -> None: self.code = None diff --git a/glitch/analysis/security.py b/glitch/analysis/security.py index 05e5a183..aed7fbeb 100644 --- a/glitch/analysis/security.py +++ b/glitch/analysis/security.py @@ -27,12 +27,15 @@ def check(self, element, file: str) -> List[Error]: class DockerNonOfficialImageSmell(SmellChecker): def check(self, element, file: str) -> List[Error]: - if not isinstance(element, UnitBlock) or \ - element.name is None or "Dockerfile" in element.name: + if ( + not isinstance(element, UnitBlock) + or element.name is None + or "Dockerfile" in element.name + ): return [] image = element.name.split(":") if image[0] not in SecurityVisitor._DOCKER_OFFICIAL_IMAGES: - return [Error('sec_non_official_image', element, file, repr(element))] + return [Error("sec_non_official_image", element, file, repr(element))] return [] def __init__(self, tech: Tech) -> None: @@ -55,58 +58,126 @@ def get_name() -> str: def config(self, config_path: str): config = configparser.ConfigParser() config.read(config_path) - SecurityVisitor.__WRONG_WORDS = json.loads(config['security']['suspicious_words']) - SecurityVisitor.__PASSWORDS = json.loads(config['security']['passwords']) - SecurityVisitor.__USERS = json.loads(config['security']['users']) - SecurityVisitor.__PROFILE = json.loads(config['security']['profile']) - SecurityVisitor.__SECRETS = json.loads(config['security']['secrets']) - SecurityVisitor.__MISC_SECRETS = json.loads(config['security']['misc_secrets']) - SecurityVisitor.__ROLES = json.loads(config['security']['roles']) - SecurityVisitor.__DOWNLOAD = json.loads(config['security']['download_extensions']) - SecurityVisitor.__SSH_DIR = json.loads(config['security']['ssh_dirs']) - SecurityVisitor.__ADMIN = json.loads(config['security']['admin']) - SecurityVisitor.__CHECKSUM = json.loads(config['security']['checksum']) - SecurityVisitor.__CRYPT = json.loads(config['security']['weak_crypt']) - SecurityVisitor.__CRYPT_WHITELIST = json.loads(config['security']['weak_crypt_whitelist']) - SecurityVisitor.__URL_WHITELIST = json.loads(config['security']['url_http_white_list']) - SecurityVisitor.__SECRETS_WHITELIST = json.loads(config['security']['secrets_white_list']) - SecurityVisitor.__SENSITIVE_DATA = json.loads(config['security']['sensitive_data']) - SecurityVisitor.__SECRET_ASSIGN = json.loads(config['security']['secret_value_assign']) - SecurityVisitor.__GITHUB_ACTIONS = json.loads(config['security']['github_actions_resources']) - + SecurityVisitor.__WRONG_WORDS = json.loads( + config["security"]["suspicious_words"] + ) + SecurityVisitor.__PASSWORDS = json.loads(config["security"]["passwords"]) + SecurityVisitor.__USERS = json.loads(config["security"]["users"]) + SecurityVisitor.__PROFILE = json.loads(config["security"]["profile"]) + SecurityVisitor.__SECRETS = json.loads(config["security"]["secrets"]) + SecurityVisitor.__MISC_SECRETS = json.loads(config["security"]["misc_secrets"]) + SecurityVisitor.__ROLES = json.loads(config["security"]["roles"]) + SecurityVisitor.__DOWNLOAD = json.loads( + config["security"]["download_extensions"] + ) + SecurityVisitor.__SSH_DIR = json.loads(config["security"]["ssh_dirs"]) + SecurityVisitor.__ADMIN = json.loads(config["security"]["admin"]) + SecurityVisitor.__CHECKSUM = json.loads(config["security"]["checksum"]) + SecurityVisitor.__CRYPT = json.loads(config["security"]["weak_crypt"]) + SecurityVisitor.__CRYPT_WHITELIST = json.loads( + config["security"]["weak_crypt_whitelist"] + ) + SecurityVisitor.__URL_WHITELIST = json.loads( + config["security"]["url_http_white_list"] + ) + SecurityVisitor.__SECRETS_WHITELIST = json.loads( + config["security"]["secrets_white_list"] + ) + SecurityVisitor.__SENSITIVE_DATA = json.loads( + config["security"]["sensitive_data"] + ) + SecurityVisitor.__SECRET_ASSIGN = json.loads( + config["security"]["secret_value_assign"] + ) + SecurityVisitor.__GITHUB_ACTIONS = json.loads( + config["security"]["github_actions_resources"] + ) + if self.tech == Tech.terraform: - SecurityVisitor._INTEGRITY_POLICY = json.loads(config['security']['integrity_policy']) - SecurityVisitor._HTTPS_CONFIGS = json.loads(config['security']['ensure_https']) - SecurityVisitor._SSL_TLS_POLICY = json.loads(config['security']['ssl_tls_policy']) - SecurityVisitor._DNSSEC_CONFIGS = json.loads(config['security']['ensure_dnssec']) - SecurityVisitor._PUBLIC_IP_CONFIGS = json.loads(config['security']['use_public_ip']) - SecurityVisitor._POLICY_KEYWORDS = json.loads(config['security']['policy_keywords']) - SecurityVisitor._ACCESS_CONTROL_CONFIGS = json.loads(config['security']['insecure_access_control']) - SecurityVisitor._AUTHENTICATION = json.loads(config['security']['authentication']) - SecurityVisitor._POLICY_ACCESS_CONTROL = json.loads(config['security']['policy_insecure_access_control']) - SecurityVisitor._POLICY_AUTHENTICATION = json.loads(config['security']['policy_authentication']) - SecurityVisitor._MISSING_ENCRYPTION = json.loads(config['security']['missing_encryption']) - SecurityVisitor._CONFIGURATION_KEYWORDS = json.loads(config['security']['configuration_keywords']) - SecurityVisitor._ENCRYPT_CONFIG = json.loads(config['security']['encrypt_configuration']) - SecurityVisitor._FIREWALL_CONFIGS = json.loads(config['security']['firewall']) - SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS = json.loads(config['security']['missing_threats_detection_alerts']) - SecurityVisitor._PASSWORD_KEY_POLICY = json.loads(config['security']['password_key_policy']) - SecurityVisitor._KEY_MANAGEMENT = json.loads(config['security']['key_management']) - SecurityVisitor._NETWORK_SECURITY_RULES = json.loads(config['security']['network_security_rules']) - SecurityVisitor._PERMISSION_IAM_POLICIES = json.loads(config['security']['permission_iam_policies']) - SecurityVisitor._GOOGLE_IAM_MEMBER = json.loads(config['security']['google_iam_member_resources']) - SecurityVisitor._LOGGING = json.loads(config['security']['logging']) - SecurityVisitor._GOOGLE_SQL_DATABASE_LOG_FLAGS = json.loads(config['security']['google_sql_database_log_flags']) - SecurityVisitor._POSSIBLE_ATTACHED_RESOURCES = json.loads(config['security']['possible_attached_resources_aws_route53']) - SecurityVisitor._VERSIONING = json.loads(config['security']['versioning']) - SecurityVisitor._NAMING = json.loads(config['security']['naming']) - SecurityVisitor._REPLICATION = json.loads(config['security']['replication']) - - SecurityVisitor.__FILE_COMMANDS = json.loads(config['security']['file_commands']) - SecurityVisitor.__SHELL_RESOURCES = json.loads(config['security']['shell_resources']) - SecurityVisitor.__IP_BIND_COMMANDS = json.loads(config['security']['ip_binding_commands']) + SecurityVisitor._INTEGRITY_POLICY = json.loads( + config["security"]["integrity_policy"] + ) + SecurityVisitor._HTTPS_CONFIGS = json.loads( + config["security"]["ensure_https"] + ) + SecurityVisitor._SSL_TLS_POLICY = json.loads( + config["security"]["ssl_tls_policy"] + ) + SecurityVisitor._DNSSEC_CONFIGS = json.loads( + config["security"]["ensure_dnssec"] + ) + SecurityVisitor._PUBLIC_IP_CONFIGS = json.loads( + config["security"]["use_public_ip"] + ) + SecurityVisitor._POLICY_KEYWORDS = json.loads( + config["security"]["policy_keywords"] + ) + SecurityVisitor._ACCESS_CONTROL_CONFIGS = json.loads( + config["security"]["insecure_access_control"] + ) + SecurityVisitor._AUTHENTICATION = json.loads( + config["security"]["authentication"] + ) + SecurityVisitor._POLICY_ACCESS_CONTROL = json.loads( + config["security"]["policy_insecure_access_control"] + ) + SecurityVisitor._POLICY_AUTHENTICATION = json.loads( + config["security"]["policy_authentication"] + ) + SecurityVisitor._MISSING_ENCRYPTION = json.loads( + config["security"]["missing_encryption"] + ) + SecurityVisitor._CONFIGURATION_KEYWORDS = json.loads( + config["security"]["configuration_keywords"] + ) + SecurityVisitor._ENCRYPT_CONFIG = json.loads( + config["security"]["encrypt_configuration"] + ) + SecurityVisitor._FIREWALL_CONFIGS = json.loads( + config["security"]["firewall"] + ) + SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS = json.loads( + config["security"]["missing_threats_detection_alerts"] + ) + SecurityVisitor._PASSWORD_KEY_POLICY = json.loads( + config["security"]["password_key_policy"] + ) + SecurityVisitor._KEY_MANAGEMENT = json.loads( + config["security"]["key_management"] + ) + SecurityVisitor._NETWORK_SECURITY_RULES = json.loads( + config["security"]["network_security_rules"] + ) + SecurityVisitor._PERMISSION_IAM_POLICIES = json.loads( + config["security"]["permission_iam_policies"] + ) + SecurityVisitor._GOOGLE_IAM_MEMBER = json.loads( + config["security"]["google_iam_member_resources"] + ) + SecurityVisitor._LOGGING = json.loads(config["security"]["logging"]) + SecurityVisitor._GOOGLE_SQL_DATABASE_LOG_FLAGS = json.loads( + config["security"]["google_sql_database_log_flags"] + ) + SecurityVisitor._POSSIBLE_ATTACHED_RESOURCES = json.loads( + config["security"]["possible_attached_resources_aws_route53"] + ) + SecurityVisitor._VERSIONING = json.loads(config["security"]["versioning"]) + SecurityVisitor._NAMING = json.loads(config["security"]["naming"]) + SecurityVisitor._REPLICATION = json.loads(config["security"]["replication"]) + + SecurityVisitor.__FILE_COMMANDS = json.loads( + config["security"]["file_commands"] + ) + SecurityVisitor.__SHELL_RESOURCES = json.loads( + config["security"]["shell_resources"] + ) + SecurityVisitor.__IP_BIND_COMMANDS = json.loads( + config["security"]["ip_binding_commands"] + ) SecurityVisitor.__OBSOLETE_COMMANDS = self._load_data_file("obsolete_commands") - SecurityVisitor._DOCKER_OFFICIAL_IMAGES = self._load_data_file("official_docker_images") + SecurityVisitor._DOCKER_OFFICIAL_IMAGES = self._load_data_file( + "official_docker_images" + ) @staticmethod def _load_data_file(file: str) -> List[str]: @@ -132,70 +203,80 @@ def check_atomicunit(self, au: AtomicUnit, file: str) -> List[Error]: if not isinstance(value, str): continue if a.name in ["mode", "m"] and re.search( - r'(?:^0?777$)|(?:(?:^|(?:ugo)|o|a)\+[rwx]{3})', - value - ): - errors.append(Error('sec_full_permission_filesystem', a, file, repr(a))) + r"(?:^0?777$)|(?:(?:^|(?:ugo)|o|a)\+[rwx]{3})", value + ): + errors.append( + Error("sec_full_permission_filesystem", a, file, repr(a)) + ) if au.type in SecurityVisitor.__OBSOLETE_COMMANDS: - errors.append(Error('sec_obsolete_command', au, file, repr(au))) + errors.append(Error("sec_obsolete_command", au, file, repr(au))) elif any(au.type.endswith(res) for res in SecurityVisitor.__SHELL_RESOURCES): for attr in au.attributes: - if isinstance(attr.value, str) and attr.value.split(" ")[0] in SecurityVisitor.__OBSOLETE_COMMANDS: - errors.append(Error('sec_obsolete_command', attr, file, repr(attr))) + if ( + isinstance(attr.value, str) + and attr.value.split(" ")[0] in SecurityVisitor.__OBSOLETE_COMMANDS + ): + errors.append(Error("sec_obsolete_command", attr, file, repr(attr))) break - + for checker in self.checkers: checker.code = self.code errors += checker.check(au, file) - + if self.__is_http_url(au.name): - errors.append(Error('sec_https', au, file, repr(au))) + errors.append(Error("sec_https", au, file, repr(au))) if self.__is_weak_crypt(au.type, au.name): - errors.append(Error('sec_weak_crypt', au, file, repr(au))) + errors.append(Error("sec_weak_crypt", au, file, repr(au))) return errors def check_dependency(self, d: Dependency, file: str) -> List[Error]: return [] - def __check_keyvalue(self, c: KeyValue, file: str, au_type = None, parent_name: str = ""): + def __check_keyvalue( + self, c: KeyValue, file: str, au_type=None, parent_name: str = "" + ): errors = [] c.name = c.name.strip().lower() - - if (isinstance(c.value, type(None))): + + if isinstance(c.value, type(None)): for child in c.keyvalues: errors += self.check_element(child, file, au_type, c.name) return errors - elif (isinstance(c.value, str)): + elif isinstance(c.value, str): c.value = c.value.strip().lower() else: errors += self.check_element(c.value, file) c.value = repr(c.value) if self.__is_http_url(c.value): - errors.append(Error('sec_https', c, file, repr(c))) - - if re.match(r'(?:https?://|^)0.0.0.0', c.value) or\ - (c.name == "ip" and c.value in {"*", '::'}) or\ - (c.name in SecurityVisitor.__IP_BIND_COMMANDS and - (c.value == True or c.value in {'*', '::'})): - errors.append(Error('sec_invalid_bind', c, file, repr(c))) + errors.append(Error("sec_https", c, file, repr(c))) + + if ( + re.match(r"(?:https?://|^)0.0.0.0", c.value) + or (c.name == "ip" and c.value in {"*", "::"}) + or ( + c.name in SecurityVisitor.__IP_BIND_COMMANDS + and (c.value == True or c.value in {"*", "::"}) + ) + ): + errors.append(Error("sec_invalid_bind", c, file, repr(c))) if self.__is_weak_crypt(c.value, c.name): - errors.append(Error('sec_weak_crypt', c, file, repr(c))) + errors.append(Error("sec_weak_crypt", c, file, repr(c))) for check in SecurityVisitor.__CHECKSUM: - if (check in c.name and (c.value == 'no' or c.value == 'false')): - errors.append(Error('sec_no_int_check', c, file, repr(c))) + if check in c.name and (c.value == "no" or c.value == "false"): + errors.append(Error("sec_no_int_check", c, file, repr(c))) break - for item in (SecurityVisitor.__ROLES + SecurityVisitor.__USERS): - if (re.match(r'[_A-Za-z0-9$\/\.\[\]-]*{text}\b'.format(text=item), c.name)): - if (len(c.value) > 0 and not c.has_variable): + for item in SecurityVisitor.__ROLES + SecurityVisitor.__USERS: + if re.match(r"[_A-Za-z0-9$\/\.\[\]-]*{text}\b".format(text=item), c.name): + if len(c.value) > 0 and not c.has_variable: for admin in SecurityVisitor.__ADMIN: if admin in c.value: - errors.append(Error('sec_def_admin', c, file, repr(c))) + errors.append(Error("sec_def_admin", c, file, repr(c))) break def get_au(c, name: str, type: str): @@ -211,7 +292,7 @@ def get_au(c, name: str, type: str): return au elif isinstance(c, UnitBlock): for au in c.atomic_units: - if (au.type == type and au.name == name): + if au.type == type and au.name == name: return au return None @@ -234,62 +315,81 @@ def get_module_var(c, name: str): # only for terraform var = None - if (c.has_variable and self.tech == Tech.terraform): - value = re.sub(r'^\${(.*)}$', r'\1', c.value) - if value.startswith("var."): # input variable (atomic unit with type variable) + if c.has_variable and self.tech == Tech.terraform: + value = re.sub(r"^\${(.*)}$", r"\1", c.value) + if value.startswith( + "var." + ): # input variable (atomic unit with type variable) au = get_au(self.code, value.strip("var."), "variable") if au != None: for attribute in au.attributes: if attribute.name == "default": var = attribute - elif value.startswith("local."): # local value (variable) + elif value.startswith("local."): # local value (variable) var = get_module_var(self.code, value.strip("local.")) - for item in (SecurityVisitor.__PASSWORDS + - SecurityVisitor.__SECRETS + SecurityVisitor.__USERS): - if (re.match(r'[_A-Za-z0-9$\/\.\[\]-]*{text}\b'.format(text=item), c.name) - and c.name.split("[")[0] not in SecurityVisitor.__SECRETS_WHITELIST + SecurityVisitor.__PROFILE): - if (not c.has_variable or var): - + for item in ( + SecurityVisitor.__PASSWORDS + + SecurityVisitor.__SECRETS + + SecurityVisitor.__USERS + ): + if ( + re.match(r"[_A-Za-z0-9$\/\.\[\]-]*{text}\b".format(text=item), c.name) + and c.name.split("[")[0] + not in SecurityVisitor.__SECRETS_WHITELIST + SecurityVisitor.__PROFILE + ): + if not c.has_variable or var: if not c.has_variable: - if (item in SecurityVisitor.__PASSWORDS and len(c.value) == 0): - errors.append(Error('sec_empty_pass', c, file, repr(c))) + if item in SecurityVisitor.__PASSWORDS and len(c.value) == 0: + errors.append(Error("sec_empty_pass", c, file, repr(c))) break if var is not None: - if (item in SecurityVisitor.__PASSWORDS and var.value != None and len(var.value) == 0): - errors.append(Error('sec_empty_pass', c, file, repr(c))) + if ( + item in SecurityVisitor.__PASSWORDS + and var.value != None + and len(var.value) == 0 + ): + errors.append(Error("sec_empty_pass", c, file, repr(c))) break - errors.append(Error('sec_hard_secr', c, file, repr(c))) - if (item in SecurityVisitor.__PASSWORDS): - errors.append(Error('sec_hard_pass', c, file, repr(c))) - elif (item in SecurityVisitor.__USERS): - errors.append(Error('sec_hard_user', c, file, repr(c))) + errors.append(Error("sec_hard_secr", c, file, repr(c))) + if item in SecurityVisitor.__PASSWORDS: + errors.append(Error("sec_hard_pass", c, file, repr(c))) + elif item in SecurityVisitor.__USERS: + errors.append(Error("sec_hard_user", c, file, repr(c))) break for item in SecurityVisitor.__SSH_DIR: if item.lower() in c.name: - if len(c.value) > 0 and '/id_rsa' in c.value: - errors.append(Error('sec_hard_secr', c, file, repr(c))) + if len(c.value) > 0 and "/id_rsa" in c.value: + errors.append(Error("sec_hard_secr", c, file, repr(c))) for item in SecurityVisitor.__MISC_SECRETS: - if (re.match(r'([_A-Za-z0-9$-]*[-_]{text}([-_].*)?$)|(^{text}([-_].*)?$)'.format(text=item), c.name) - and len(c.value) > 0 and not c.has_variable): - errors.append(Error('sec_hard_secr', c, file, repr(c))) + if ( + re.match( + r"([_A-Za-z0-9$-]*[-_]{text}([-_].*)?$)|(^{text}([-_].*)?$)".format( + text=item + ), + c.name, + ) + and len(c.value) > 0 + and not c.has_variable + ): + errors.append(Error("sec_hard_secr", c, file, repr(c))) for item in SecurityVisitor.__SENSITIVE_DATA: if item.lower() in c.name: - for item_value in (SecurityVisitor.__SECRET_ASSIGN): + for item_value in SecurityVisitor.__SECRET_ASSIGN: if item_value in c.value.lower(): - errors.append(Error('sec_hard_secr', c, file, repr(c))) - if ("password" in item_value): - errors.append(Error('sec_hard_pass', c, file, repr(c))) - - if (au_type in SecurityVisitor.__GITHUB_ACTIONS and c.name == "plaintext_value"): - errors.append(Error('sec_hard_secr', c, file, repr(c))) - - if (c.has_variable and var is not None): + errors.append(Error("sec_hard_secr", c, file, repr(c))) + if "password" in item_value: + errors.append(Error("sec_hard_pass", c, file, repr(c))) + + if au_type in SecurityVisitor.__GITHUB_ACTIONS and c.name == "plaintext_value": + errors.append(Error("sec_hard_secr", c, file, repr(c))) + + if c.has_variable and var is not None: c.has_variable = var.has_variable c.value = var.value @@ -299,7 +399,9 @@ def get_module_var(c, name: str): return errors - def check_attribute(self, a: Attribute, file: str, au_type = None, parent_name: str = "") -> list[Error]: + def check_attribute( + self, a: Attribute, file: str, au_type=None, parent_name: str = "" + ) -> list[Error]: return self.__check_keyvalue(a, file, au_type, parent_name) def check_variable(self, v: Variable, file: str) -> list[Error]: @@ -307,14 +409,14 @@ def check_variable(self, v: Variable, file: str) -> list[Error]: def check_comment(self, c: Comment, file: str) -> List[Error]: errors = [] - lines = c.content.split('\n') + lines = c.content.split("\n") stop = False for word in SecurityVisitor.__WRONG_WORDS: for line in lines: tokenizer = WordPunctTokenizer() tokens = tokenizer.tokenize(line.lower()) if word in tokens: - errors.append(Error('sec_susp_comm', c, file, line)) + errors.append(Error("sec_susp_comm", c, file, line)) stop = True if stop: break @@ -335,7 +437,7 @@ def check_condition(self, c: ConditionalStatement, file: str) -> List[Error]: condition = condition.else_statement if not has_default: - return errors + [Error('sec_no_default_switch', c, file, repr(c))] + return errors + [Error("sec_no_default_switch", c, file, repr(c))] return errors @@ -363,21 +465,33 @@ def check_unitblock(self, u: UnitBlock) -> List[Error]: @staticmethod def check_integrity_check(au: AtomicUnit, path: str) -> Optional[Tuple[str, Error]]: for item in SecurityVisitor.__DOWNLOAD: - if not re.search(r'(http|https|www)[^ ,]*\.{text}'.format(text=item), au.name): + if not re.search( + r"(http|https|www)[^ ,]*\.{text}".format(text=item), au.name + ): continue if SecurityVisitor.__has_integrity_check(au.attributes): return None - return os.path.basename(au.name), Error('sec_no_int_check', au, path, repr(au)) + return os.path.basename(au.name), Error( + "sec_no_int_check", au, path, repr(au) + ) for a in au.attributes: - value = a.value.strip().lower() if isinstance(a.value, str) else repr(a.value).strip().lower() + value = ( + a.value.strip().lower() + if isinstance(a.value, str) + else repr(a.value).strip().lower() + ) for item in SecurityVisitor.__DOWNLOAD: - if not re.search(r'(http|https|www)[^ ,]*\.{text}'.format(text=item), value): + if not re.search( + r"(http|https|www)[^ ,]*\.{text}".format(text=item), value + ): continue if SecurityVisitor.__has_integrity_check(au.attributes): return None - return os.path.basename(a.value), Error('sec_no_int_check', au, path, repr(a)) + return os.path.basename(a.value), Error( + "sec_no_int_check", au, path, repr(a) + ) return None @staticmethod @@ -388,7 +502,11 @@ def check_has_checksum(au: AtomicUnit) -> Optional[str]: return os.path.basename(au.name) for a in au.attributes: - value = a.value.strip().lower() if isinstance(a.value, str) else repr(a.value).strip().lower() + value = ( + a.value.strip().lower() + if isinstance(a.value, str) + else repr(a.value).strip().lower() + ) if any(d in value for d in SecurityVisitor.__DOWNLOAD): return os.path.basename(au.name) return None @@ -402,24 +520,32 @@ def __has_integrity_check(attributes: List[Attribute]) -> bool: @staticmethod def __is_http_url(value: str) -> bool: - if (re.match(SecurityVisitor.__URL_REGEX, value) and - ('http' in value or 'www' in value) and 'https' not in value): + if ( + re.match(SecurityVisitor.__URL_REGEX, value) + and ("http" in value or "www" in value) + and "https" not in value + ): return True try: parsed_url = urlparse(value) - return parsed_url.scheme == 'http' and \ - parsed_url.hostname not in SecurityVisitor.__URL_WHITELIST + return ( + parsed_url.scheme == "http" + and parsed_url.hostname not in SecurityVisitor.__URL_WHITELIST + ) except ValueError: return False @staticmethod def __is_weak_crypt(value: str, name: str) -> bool: if any(crypt in value for crypt in SecurityVisitor.__CRYPT): - whitelist = any(word in name or word in value for word in SecurityVisitor.__CRYPT_WHITELIST) + whitelist = any( + word in name or word in value + for word in SecurityVisitor.__CRYPT_WHITELIST + ) return not whitelist return False # NOTE: in the end of the file to avoid circular import # Imports all the classes defined in the __init__.py file -from glitch.analysis.terraform import * \ No newline at end of file +from glitch.analysis.terraform import * diff --git a/glitch/analysis/terraform/__init__.py b/glitch/analysis/terraform/__init__.py index c8dd9235..3d8d8a6e 100644 --- a/glitch/analysis/terraform/__init__.py +++ b/glitch/analysis/terraform/__init__.py @@ -4,4 +4,4 @@ for file in os.listdir(os.path.dirname(__file__)): if file.endswith(".py") and file != "__init__.py": - __all__.append(file[:-3]) \ No newline at end of file + __all__.append(file[:-3]) diff --git a/glitch/analysis/terraform/access_control.py b/glitch/analysis/terraform/access_control.py index b1c96e42..3340a8ee 100644 --- a/glitch/analysis/terraform/access_control.py +++ b/glitch/analysis/terraform/access_control.py @@ -7,84 +7,198 @@ class TerraformAccessControl(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for item in SecurityVisitor._POLICY_KEYWORDS: if item.lower() == attribute.name: for config in SecurityVisitor._POLICY_ACCESS_CONTROL: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() + expr = config["keyword"].lower() + "\s*" + config["value"].lower() pattern = re.compile(rf"{expr}") - allow_expr = "\"effect\":" + "\s*" + "\"allow\"" + allow_expr = '"effect":' + "\s*" + '"allow"' allow_pattern = re.compile(rf"{allow_expr}") - if re.search(pattern, attribute.value) and re.search(allow_pattern, attribute.value): - return [Error('sec_access_control', attribute, file, repr(attribute))] + if re.search(pattern, attribute.value) and re.search( + allow_pattern, attribute.value + ): + return [ + Error( + "sec_access_control", attribute, file, repr(attribute) + ) + ] - if (re.search(r"actions\[\d+\]", attribute.name) and parent_name == "permissions" - and atomic_unit.type == "resource.azurerm_role_definition" and attribute.value == "*"): - return [Error('sec_access_control', attribute, file, repr(attribute))] - elif (((re.search(r"members\[\d+\]", attribute.name) and atomic_unit.type == "resource.google_storage_bucket_iam_binding") - or (attribute.name == "member" and atomic_unit.type == "resource.google_storage_bucket_iam_member")) - and (attribute.value == "allusers" or attribute.value == "allauthenticatedusers")): - return [Error('sec_access_control', attribute, file, repr(attribute))] - elif (attribute.name == "email" and parent_name == "service_account" + if ( + re.search(r"actions\[\d+\]", attribute.name) + and parent_name == "permissions" + and atomic_unit.type == "resource.azurerm_role_definition" + and attribute.value == "*" + ): + return [Error("sec_access_control", attribute, file, repr(attribute))] + elif ( + ( + re.search(r"members\[\d+\]", attribute.name) + and atomic_unit.type == "resource.google_storage_bucket_iam_binding" + ) + or ( + attribute.name == "member" + and atomic_unit.type == "resource.google_storage_bucket_iam_member" + ) + ) and ( + attribute.value == "allusers" or attribute.value == "allauthenticatedusers" + ): + return [Error("sec_access_control", attribute, file, repr(attribute))] + elif ( + attribute.name == "email" + and parent_name == "service_account" and atomic_unit.type == "resource.google_compute_instance" - and re.search(r".-compute@developer.gserviceaccount.com", attribute.value)): - return [Error('sec_access_control', attribute, file, repr(attribute))] + and re.search(r".-compute@developer.gserviceaccount.com", attribute.value) + ): + return [Error("sec_access_control", attribute, file, repr(attribute))] for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and not attribute.has_variable - and attribute.value.lower() not in config['values'] - and config['values'] != [""]): - return [Error('sec_access_control', attribute, file, repr(attribute))] - + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + and config["values"] != [""] + ): + return [Error("sec_access_control", attribute, file, repr(attribute))] + return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_api_gateway_method"): - http_method = self.check_required_attribute(element.attributes, [""], 'http_method') - authorization =self.check_required_attribute(element.attributes, [""], 'authorization') - if (http_method and authorization): - if (http_method.value.lower() == 'get' and authorization.value.lower() == 'none'): - api_key_required = self.check_required_attribute(element.attributes, [""], 'api_key_required') - if api_key_required and f"{api_key_required.value}".lower() != 'true': - errors.append(Error('sec_access_control', api_key_required, file, repr(api_key_required))) + if element.type == "resource.aws_api_gateway_method": + http_method = self.check_required_attribute( + element.attributes, [""], "http_method" + ) + authorization = self.check_required_attribute( + element.attributes, [""], "authorization" + ) + if http_method and authorization: + if ( + http_method.value.lower() == "get" + and authorization.value.lower() == "none" + ): + api_key_required = self.check_required_attribute( + element.attributes, [""], "api_key_required" + ) + if ( + api_key_required + and f"{api_key_required.value}".lower() != "true" + ): + errors.append( + Error( + "sec_access_control", + api_key_required, + file, + repr(api_key_required), + ) + ) elif not api_key_required: - errors.append(Error('sec_access_control', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'api_key_required'.")) - elif (http_method and not authorization): - errors.append(Error('sec_access_control', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'authorization'.")) - elif (element.type == "resource.github_repository"): - visibility = self.check_required_attribute(element.attributes, [""], 'visibility') + errors.append( + Error( + "sec_access_control", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name 'api_key_required'.", + ) + ) + elif http_method and not authorization: + errors.append( + Error( + "sec_access_control", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name 'authorization'.", + ) + ) + elif element.type == "resource.github_repository": + visibility = self.check_required_attribute( + element.attributes, [""], "visibility" + ) if visibility is not None: if visibility.value.lower() not in ["private", "internal"]: - errors.append(Error('sec_access_control', visibility, file, repr(visibility))) + errors.append( + Error( + "sec_access_control", visibility, file, repr(visibility) + ) + ) else: - private = self.check_required_attribute(element.attributes, [""], 'private') + private = self.check_required_attribute( + element.attributes, [""], "private" + ) if private is not None: if f"{private.value}".lower() != "true": - errors.append(Error('sec_access_control', private, file, repr(private))) + errors.append( + Error( + "sec_access_control", private, file, repr(private) + ) + ) else: - errors.append(Error('sec_access_control', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'visibility' or 'private'.")) - elif (element.type == "resource.google_sql_database_instance"): - errors += self.check_database_flags(element, file, 'sec_access_control', "cross db ownership chaining", "off") - elif (element.type == "resource.aws_s3_bucket"): + errors.append( + Error( + "sec_access_control", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name 'visibility' or 'private'.", + ) + ) + elif element.type == "resource.google_sql_database_instance": + errors += self.check_database_flags( + element, + file, + "sec_access_control", + "cross db ownership chaining", + "off", + ) + elif element.type == "resource.aws_s3_bucket": expr = "\${aws_s3_bucket\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - if self.get_associated_au(file, "resource.aws_s3_bucket_public_access_block", "bucket", pattern, [""]) is None: - errors.append(Error('sec_access_control', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' " + - f"associated to an 'aws_s3_bucket' resource.")) + if ( + self.get_associated_au( + file, + "resource.aws_s3_bucket_public_access_block", + "bucket", + pattern, + [""], + ) + is None + ): + errors.append( + Error( + "sec_access_control", + element, + file, + repr(element), + f"Suggestion: check for a required resource 'aws_s3_bucket_public_access_block' " + + f"associated to an 'aws_s3_bucket' resource.", + ) + ) for config in SecurityVisitor._ACCESS_CONTROL_CONFIGS: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_access_control', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_access_control", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/attached_resource.py b/glitch/analysis/terraform/attached_resource.py index 144de0a9..a7ff7ae2 100644 --- a/glitch/analysis/terraform/attached_resource.py +++ b/glitch/analysis/terraform/attached_resource.py @@ -8,14 +8,18 @@ class TerraformAttachedResource(TerraformSmellChecker): def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): + def check_attached_resource(attributes, resource_types): for a in attributes: if a.value != None: for resource_type in resource_types: - if (f"{a.value}".lower().startswith("${" + f"{resource_type}.") - or f"{a.value}".lower().startswith(f"{resource_type}.")): + if f"{a.value}".lower().startswith( + "${" + f"{resource_type}." + ) or f"{a.value}".lower().startswith(f"{resource_type}."): resource_name = a.value.lower().split(".")[1] - if self.get_au(file, resource_name, f"resource.{resource_type}"): + if self.get_au( + file, resource_name, f"resource.{resource_type}" + ): return True elif a.value == None: attached = check_attached_resource(a.keyvalues, resource_types) @@ -23,9 +27,15 @@ def check_attached_resource(attributes, resource_types): return True return False - if (element.type == "resource.aws_route53_record"): - type_A = self.check_required_attribute(element.attributes, [""], "type", "a") - if type_A and not check_attached_resource(element.attributes, SecurityVisitor._POSSIBLE_ATTACHED_RESOURCES): - errors.append(Error('sec_attached_resource', element, file, repr(element))) + if element.type == "resource.aws_route53_record": + type_A = self.check_required_attribute( + element.attributes, [""], "type", "a" + ) + if type_A and not check_attached_resource( + element.attributes, SecurityVisitor._POSSIBLE_ATTACHED_RESOURCES + ): + errors.append( + Error("sec_attached_resource", element, file, repr(element)) + ) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/authentication.py b/glitch/analysis/terraform/authentication.py index 0bf2a9e0..07ca6f79 100644 --- a/glitch/analysis/terraform/authentication.py +++ b/glitch/analysis/terraform/authentication.py @@ -7,45 +7,87 @@ class TerraformAuthentication(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for item in SecurityVisitor._POLICY_KEYWORDS: - if item.lower() == attribute.name: + if item.lower() == attribute.name: for config in SecurityVisitor._POLICY_AUTHENTICATION: - if atomic_unit.type in config['au_type']: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() + if atomic_unit.type in config["au_type"]: + expr = ( + config["keyword"].lower() + "\s*" + config["value"].lower() + ) pattern = re.compile(rf"{expr}") if not re.search(pattern, attribute.value): - return [Error('sec_authentication', attribute, file, repr(attribute))] + return [ + Error( + "sec_authentication", + attribute, + file, + repr(attribute), + ) + ] for config in SecurityVisitor._AUTHENTICATION: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and not attribute.has_variable - and attribute.value.lower() not in config['values'] - and config['values'] != [""]): - return [Error('sec_authentication', attribute, file, repr(attribute))] - + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + and config["values"] != [""] + ): + return [Error("sec_authentication", attribute, file, repr(attribute))] + return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): - if (element.type == "resource.google_sql_database_instance"): - errors += self.check_database_flags(element, file, 'sec_authentication', "contained database authentication", "off") - elif (element.type == "resource.aws_iam_group"): + if element.type == "resource.google_sql_database_instance": + errors += self.check_database_flags( + element, + file, + "sec_authentication", + "contained database authentication", + "off", + ) + elif element.type == "resource.aws_iam_group": expr = "\${aws_iam_group\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - if not self.get_associated_au(file, "resource.aws_iam_group_policy", "group", pattern, [""]): - errors.append(Error('sec_authentication', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an " + - f"'aws_iam_group' resource.")) + if not self.get_associated_au( + file, "resource.aws_iam_group_policy", "group", pattern, [""] + ): + errors.append( + Error( + "sec_authentication", + element, + file, + repr(element), + f"Suggestion: check for a required resource 'aws_iam_group_policy' associated to an " + + f"'aws_iam_group' resource.", + ) + ) for config in SecurityVisitor._AUTHENTICATION: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_authentication', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_authentication", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/dns_policy.py b/glitch/analysis/terraform/dns_policy.py index c683792e..4712814e 100644 --- a/glitch/analysis/terraform/dns_policy.py +++ b/glitch/analysis/terraform/dns_policy.py @@ -6,24 +6,42 @@ class TerraformDnsWithoutDnssec(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for config in SecurityVisitor._DNSSEC_CONFIGS: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and not attribute.has_variable - and attribute.value.lower() not in config['values'] - and config['values'] != [""]): - return [Error('sec_dnssec', attribute, file, repr(attribute))] + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + and config["values"] != [""] + ): + return [Error("sec_dnssec", attribute, file, repr(attribute))] return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._DNSSEC_CONFIGS: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_dnssec', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_dnssec", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - - return errors \ No newline at end of file + + return errors diff --git a/glitch/analysis/terraform/firewall_misconfig.py b/glitch/analysis/terraform/firewall_misconfig.py index 2723735b..2d783c8c 100644 --- a/glitch/analysis/terraform/firewall_misconfig.py +++ b/glitch/analysis/terraform/firewall_misconfig.py @@ -6,26 +6,58 @@ class TerraformFirewallMisconfig(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for config in SecurityVisitor._FIREWALL_CONFIGS: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - return [Error('sec_firewall_misconfig', attribute, file, repr(attribute))] - elif ("any_not_empty" not in config['values'] and not attribute.has_variable and - attribute.value.lower() not in config['values']): - return [Error('sec_firewall_misconfig', attribute, file, repr(attribute))] + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and config["values"] != [""] + ): + if ( + "any_not_empty" in config["values"] + and attribute.value.lower() == "" + ): + return [ + Error( + "sec_firewall_misconfig", attribute, file, repr(attribute) + ) + ] + elif ( + "any_not_empty" not in config["values"] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + ): + return [ + Error( + "sec_firewall_misconfig", attribute, file, repr(attribute) + ) + ] return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._FIREWALL_CONFIGS: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_firewall_misconfig', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_firewall_misconfig", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/http_without_tls.py b/glitch/analysis/terraform/http_without_tls.py index 98855593..37b3bc30 100644 --- a/glitch/analysis/terraform/http_without_tls.py +++ b/glitch/analysis/terraform/http_without_tls.py @@ -6,21 +6,27 @@ class TerraformHttpWithoutTls(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for config in SecurityVisitor._HTTPS_CONFIGS: - if (attribute.name == config["attribute"] and atomic_unit.type in config["au_type"] - and parent_name in config["parents"] and not attribute.has_variable - and attribute.value.lower() not in config["values"]): - return [Error('sec_https', attribute, file, repr(attribute))] - + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + ): + return [Error("sec_https", attribute, file, repr(attribute))] + return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): - if (element.type == "data.http"): + if element.type == "data.http": url = self.check_required_attribute(element.attributes, [""], "url") - if ("${" in url.value): + if "${" in url.value: vars = url.value.split("${") r = url.value.split("${")[1].split("}")[0] for var in vars: @@ -36,14 +42,26 @@ def check(self, element, file: str): resource_type = r.split(".")[0] resource_name = r.split(".")[1] if self.get_au(file, resource_name, type + "." + resource_type): - errors.append(Error('sec_https', url, file, repr(url))) + errors.append(Error("sec_https", url, file, repr(url))) for config in SecurityVisitor._HTTPS_CONFIGS: - if (config["required"] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config["parents"], config['attribute'])): - errors.append(Error('sec_https', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_https", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - - return errors \ No newline at end of file + + return errors diff --git a/glitch/analysis/terraform/integrity_policy.py b/glitch/analysis/terraform/integrity_policy.py index 584921a8..7202b87f 100644 --- a/glitch/analysis/terraform/integrity_policy.py +++ b/glitch/analysis/terraform/integrity_policy.py @@ -6,24 +6,42 @@ class TerraformIntegrityPolicy(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for policy in SecurityVisitor._INTEGRITY_POLICY: - if (attribute.name == policy['attribute'] and atomic_unit.type in policy['au_type'] - and parent_name in policy['parents'] and not attribute.has_variable - and attribute.value.lower() not in policy['values']): - return [Error('sec_integrity_policy', attribute, file, repr(attribute))] - + if ( + attribute.name == policy["attribute"] + and atomic_unit.type in policy["au_type"] + and parent_name in policy["parents"] + and not attribute.has_variable + and attribute.value.lower() not in policy["values"] + ): + return [Error("sec_integrity_policy", attribute, file, repr(attribute))] + return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for policy in SecurityVisitor._INTEGRITY_POLICY: - if (policy['required'] == "yes" and element.type in policy['au_type'] - and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): - errors.append(Error('sec_integrity_policy', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - + if ( + policy["required"] == "yes" + and element.type in policy["au_type"] + and not self.check_required_attribute( + element.attributes, policy["parents"], policy["attribute"] + ) + ): + errors.append( + Error( + "sec_integrity_policy", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/key_management.py b/glitch/analysis/terraform/key_management.py index 05db180e..67e69856 100644 --- a/glitch/analysis/terraform/key_management.py +++ b/glitch/analysis/terraform/key_management.py @@ -7,48 +7,100 @@ class TerraformKeyManagement(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for config in SecurityVisitor._KEY_MANAGEMENT: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - return [Error('sec_key_management', attribute, file, repr(attribute))] - elif ("any_not_empty" not in config['values'] and not attribute.has_variable and - attribute.value.lower() not in config['values']): - return [Error('sec_key_management', attribute, file, repr(attribute))] + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and config["values"] != [""] + ): + if ( + "any_not_empty" in config["values"] + and attribute.value.lower() == "" + ): + return [ + Error("sec_key_management", attribute, file, repr(attribute)) + ] + elif ( + "any_not_empty" not in config["values"] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + ): + return [ + Error("sec_key_management", attribute, file, repr(attribute)) + ] - if (attribute.name == "rotation_period" and atomic_unit.type == "resource.google_kms_crypto_key"): - expr1 = r'\d+\.\d{0,9}s' - expr2 = r'\d+s' - if (re.search(expr1, attribute.value) or re.search(expr2, attribute.value)): - if (int(attribute.value.split("s")[0]) > 7776000): - return [Error('sec_key_management', attribute, file, repr(attribute))] + if ( + attribute.name == "rotation_period" + and atomic_unit.type == "resource.google_kms_crypto_key" + ): + expr1 = r"\d+\.\d{0,9}s" + expr2 = r"\d+s" + if re.search(expr1, attribute.value) or re.search(expr2, attribute.value): + if int(attribute.value.split("s")[0]) > 7776000: + return [ + Error("sec_key_management", attribute, file, repr(attribute)) + ] else: - return [Error('sec_key_management', attribute, file, repr(attribute))] - elif (attribute.name == "kms_master_key_id" and ((atomic_unit.type == "resource.aws_sqs_queue" - and attribute.value == "alias/aws/sqs") or (atomic_unit.type == "resource.aws_sns_queue" - and attribute.value == "alias/aws/sns"))): - return [Error('sec_key_management', attribute, file, repr(attribute))] - + return [Error("sec_key_management", attribute, file, repr(attribute))] + elif attribute.name == "kms_master_key_id" and ( + ( + atomic_unit.type == "resource.aws_sqs_queue" + and attribute.value == "alias/aws/sqs" + ) + or ( + atomic_unit.type == "resource.aws_sns_queue" + and attribute.value == "alias/aws/sns" + ) + ): + return [Error("sec_key_management", attribute, file, repr(attribute))] + return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): - if (element.type == "resource.azurerm_storage_account"): + if element.type == "resource.azurerm_storage_account": expr = "\${azurerm_storage_account\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - if not self.get_associated_au(file, "resource.azurerm_storage_account_customer_managed_key", "storage_account_id", - pattern, [""]): - errors.append(Error('sec_key_management', element, file, repr(element), - f"Suggestion: check for a required resource 'azurerm_storage_account_customer_managed_key' " + - f"associated to an 'azurerm_storage_account' resource.")) + if not self.get_associated_au( + file, + "resource.azurerm_storage_account_customer_managed_key", + "storage_account_id", + pattern, + [""], + ): + errors.append( + Error( + "sec_key_management", + element, + file, + repr(element), + f"Suggestion: check for a required resource 'azurerm_storage_account_customer_managed_key' " + + f"associated to an 'azurerm_storage_account' resource.", + ) + ) for config in SecurityVisitor._KEY_MANAGEMENT: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_key_management', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_key_management", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - - return errors \ No newline at end of file + + return errors diff --git a/glitch/analysis/terraform/logging.py b/glitch/analysis/terraform/logging.py index 7d46243d..c27b2432 100644 --- a/glitch/analysis/terraform/logging.py +++ b/glitch/analysis/terraform/logging.py @@ -9,257 +9,468 @@ class TerraformLogging(TerraformSmellChecker): def __check_log_attribute( - self, - element, - attribute_name: str, - file: str, - values: List[str], - all: bool = False - ): + self, + element, + attribute_name: str, + file: str, + values: List[str], + all: bool = False, + ): errors = [] attribute = self.check_required_attribute( - element.attributes, - [""], - f"{attribute_name}[0]" + element.attributes, [""], f"{attribute_name}[0]" ) if all: active = True for v in values[:]: attribute_checked, _ = self.iterate_required_attributes( - element.attributes, - attribute_name, - lambda x: x.value.lower() == v + element.attributes, attribute_name, lambda x: x.value.lower() == v ) if attribute_checked: values.remove(v) active = active and attribute_checked else: active, _ = self.iterate_required_attributes( - element.attributes, - attribute_name, - lambda x: x.value.lower() in values + element.attributes, attribute_name, lambda x: x.value.lower() in values ) - if attribute is None: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{attribute_name}'.")) + errors.append( + Error( + "sec_logging", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{attribute_name}'.", + ) + ) elif not active and not all: - errors.append(Error('sec_logging', attribute, file, repr(attribute))) + errors.append(Error("sec_logging", attribute, file, repr(attribute))) elif not active and all: - errors.append(Error('sec_logging', attribute, file, repr(attribute), - f"Suggestion: check for additional log type(s) {values}.")) + errors.append( + Error( + "sec_logging", + attribute, + file, + repr(attribute), + f"Suggestion: check for additional log type(s) {values}.", + ) + ) return errors - + def __check_azurerm_storage_container(self, element, file: str): errors = [] container_access_type = self.check_required_attribute( element.attributes, [""], "container_access_type" ) - if container_access_type and container_access_type.value.lower() not in ["blob", "private"]: - errors.append(Error('sec_logging', container_access_type, file, repr(container_access_type))) + if container_access_type and container_access_type.value.lower() not in [ + "blob", + "private", + ]: + errors.append( + Error( + "sec_logging", + container_access_type, + file, + repr(container_access_type), + ) + ) - storage_account_name = self.check_required_attribute(element.attributes, [""], "storage_account_name") - if not (storage_account_name is not None and storage_account_name.value.lower().startswith("${azurerm_storage_account.")): - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + - f"'azurerm_storage_account' resource in order to enable logging.") + storage_account_name = self.check_required_attribute( + element.attributes, [""], "storage_account_name" + ) + if not ( + storage_account_name is not None + and storage_account_name.value.lower().startswith( + "${azurerm_storage_account." + ) + ): + errors.append( + Error( + "sec_logging", + element, + file, + repr(element), + f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + + f"'azurerm_storage_account' resource in order to enable logging.", + ) ) return errors - - name = storage_account_name.value.lower().split('.')[1] + + name = storage_account_name.value.lower().split(".")[1] storage_account_au = self.get_au(file, name, "resource.azurerm_storage_account") if storage_account_au is None: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + - f"'azurerm_storage_account' resource in order to enable logging.") + errors.append( + Error( + "sec_logging", + element, + file, + repr(element), + f"Suggestion: 'azurerm_storage_container' resource has to be associated to an " + + f"'azurerm_storage_account' resource in order to enable logging.", + ) ) return errors - + expr = "\${azurerm_storage_account\." + f"{name}\." pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(file, "resource.azurerm_log_analytics_storage_insights", - "storage_account_id", pattern, [""]) + assoc_au = self.get_associated_au( + file, + "resource.azurerm_log_analytics_storage_insights", + "storage_account_id", + pattern, + [""], + ) if assoc_au is None: - errors.append(Error('sec_logging', storage_account_au, file, repr(storage_account_au), - f"Suggestion: check for a required resource 'azurerm_log_analytics_storage_insights' " + - f"associated to an 'azurerm_storage_account' resource.") + errors.append( + Error( + "sec_logging", + storage_account_au, + file, + repr(storage_account_au), + f"Suggestion: check for a required resource 'azurerm_log_analytics_storage_insights' " + + f"associated to an 'azurerm_storage_account' resource.", + ) ) return errors - - blob_container_names = self.check_required_attribute(assoc_au.attributes, [""], "blob_container_names[0]") + blob_container_names = self.check_required_attribute( + assoc_au.attributes, [""], "blob_container_names[0]" + ) if blob_container_names is None: - errors.append(Error('sec_logging', assoc_au, file, repr(assoc_au), - f"Suggestion: check for a required attribute with name 'blob_container_names'.") + errors.append( + Error( + "sec_logging", + assoc_au, + file, + repr(assoc_au), + f"Suggestion: check for a required attribute with name 'blob_container_names'.", + ) ) return errors - + contains_blob_name, _ = self.iterate_required_attributes( - assoc_au.attributes, - "blob_container_names", - lambda x: x.value + assoc_au.attributes, "blob_container_names", lambda x: x.value ) if not contains_blob_name: - errors.append(Error('sec_logging', assoc_au.attributes[-1], file, repr(assoc_au.attributes[-1]))) + errors.append( + Error( + "sec_logging", + assoc_au.attributes[-1], + file, + repr(assoc_au.attributes[-1]), + ) + ) return errors - - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: - if (attribute.name == "cloud_watch_logs_group_arn" and atomic_unit.type == "resource.aws_cloudtrail"): + + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: + if ( + attribute.name == "cloud_watch_logs_group_arn" + and atomic_unit.type == "resource.aws_cloudtrail" + ): if re.match(r"^\${aws_cloudwatch_log_group\..", attribute.value): - aws_cloudwatch_log_group_name = attribute.value.split('.')[1] - if not self.get_au(file, aws_cloudwatch_log_group_name, "resource.aws_cloudwatch_log_group"): - return [Error('sec_logging', attribute, file, repr(attribute), - f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + - f"with name '{aws_cloudwatch_log_group_name}'.")] + aws_cloudwatch_log_group_name = attribute.value.split(".")[1] + if not self.get_au( + file, + aws_cloudwatch_log_group_name, + "resource.aws_cloudwatch_log_group", + ): + return [ + Error( + "sec_logging", + attribute, + file, + repr(attribute), + f"Suggestion: check for a required resource 'aws_cloudwatch_log_group' " + + f"with name '{aws_cloudwatch_log_group_name}'.", + ) + ] else: - return [Error('sec_logging', attribute, file, repr(attribute))] - elif (((attribute.name == "retention_in_days" and parent_name == "" - and atomic_unit.type in ["resource.azurerm_mssql_database_extended_auditing_policy", - "resource.azurerm_mssql_server_extended_auditing_policy"]) - or (attribute.name == "days" and parent_name == "retention_policy" - and atomic_unit.type == "resource.azurerm_network_watcher_flow_log")) - and ((not attribute.value.isnumeric()) or (attribute.value.isnumeric() and int(attribute.value) < 90))): - return [Error('sec_logging', attribute, file, repr(attribute))] - elif (attribute.name == "days" and parent_name == "retention_policy" - and atomic_unit.type == "resource.azurerm_monitor_log_profile" - and (not attribute.value.isnumeric() or (attribute.value.isnumeric() and int(attribute.value) < 365))): - return [Error('sec_logging', attribute, file, repr(attribute))] + return [Error("sec_logging", attribute, file, repr(attribute))] + elif ( + ( + attribute.name == "retention_in_days" + and parent_name == "" + and atomic_unit.type + in [ + "resource.azurerm_mssql_database_extended_auditing_policy", + "resource.azurerm_mssql_server_extended_auditing_policy", + ] + ) + or ( + attribute.name == "days" + and parent_name == "retention_policy" + and atomic_unit.type == "resource.azurerm_network_watcher_flow_log" + ) + ) and ( + (not attribute.value.isnumeric()) + or (attribute.value.isnumeric() and int(attribute.value) < 90) + ): + return [Error("sec_logging", attribute, file, repr(attribute))] + elif ( + attribute.name == "days" + and parent_name == "retention_policy" + and atomic_unit.type == "resource.azurerm_monitor_log_profile" + and ( + not attribute.value.isnumeric() + or (attribute.value.isnumeric() and int(attribute.value) < 365) + ) + ): + return [Error("sec_logging", attribute, file, repr(attribute))] for config in SecurityVisitor._LOGGING: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - return [Error('sec_logging', attribute, file, repr(attribute))] - elif ("any_not_empty" not in config['values'] and not attribute.has_variable and - attribute.value.lower() not in config['values']): - return [Error('sec_logging', attribute, file, repr(attribute))] - + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and config["values"] != [""] + ): + if ( + "any_not_empty" in config["values"] + and attribute.value.lower() == "" + ): + return [Error("sec_logging", attribute, file, repr(attribute))] + elif ( + "any_not_empty" not in config["values"] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + ): + return [Error("sec_logging", attribute, file, repr(attribute))] + return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_eks_cluster"): - errors.extend(self.__check_log_attribute( - element, - "enabled_cluster_log_types", - file, - ["api", "authenticator", "audit", "scheduler", "controllermanager"], - all=True - )) - elif (element.type == "resource.aws_msk_cluster"): - broker_logs = self.check_required_attribute(element.attributes, ["logging_info"], "broker_logs") + if element.type == "resource.aws_eks_cluster": + errors.extend( + self.__check_log_attribute( + element, + "enabled_cluster_log_types", + file, + [ + "api", + "authenticator", + "audit", + "scheduler", + "controllermanager", + ], + all=True, + ) + ) + elif element.type == "resource.aws_msk_cluster": + broker_logs = self.check_required_attribute( + element.attributes, ["logging_info"], "broker_logs" + ) if broker_logs is not None: active = False logs_type = ["cloudwatch_logs", "firehose", "s3"] a_list = [] for type in logs_type: - log = self.check_required_attribute(broker_logs.keyvalues, [""], type) + log = self.check_required_attribute( + broker_logs.keyvalues, [""], type + ) if log is not None: - enabled = self.check_required_attribute(log.keyvalues, [""], "enabled") + enabled = self.check_required_attribute( + log.keyvalues, [""], "enabled" + ) if enabled and f"{enabled.value}".lower() == "true": active = True elif enabled and f"{enabled.value}".lower() != "true": a_list.append(enabled) if not active and a_list == []: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name " + - f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) + errors.append( + Error( + "sec_logging", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name " + + f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.", + ) + ) if not active and a_list != []: for a in a_list: - errors.append(Error('sec_logging', a, file, repr(a))) + errors.append(Error("sec_logging", a, file, repr(a))) else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name " + - f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.")) - elif (element.type == "resource.aws_neptune_cluster"): - errors.extend(self.__check_log_attribute( - element, - "enable_cloudwatch_logs_exports", - file, - ["audit"] - )) - elif (element.type == "resource.aws_docdb_cluster"): - errors.extend(self.__check_log_attribute( - element, - "enabled_cloudwatch_logs_exports", - file, - ["audit", "profiler"] - )) - elif (element.type == "resource.azurerm_mssql_server"): + errors.append( + Error( + "sec_logging", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name " + + f"'logging_info.broker_logs.[cloudwatch_logs/firehose/s3].enabled'.", + ) + ) + elif element.type == "resource.aws_neptune_cluster": + errors.extend( + self.__check_log_attribute( + element, "enable_cloudwatch_logs_exports", file, ["audit"] + ) + ) + elif element.type == "resource.aws_docdb_cluster": + errors.extend( + self.__check_log_attribute( + element, + "enabled_cloudwatch_logs_exports", + file, + ["audit", "profiler"], + ) + ) + elif element.type == "resource.azurerm_mssql_server": expr = "\${azurerm_mssql_server\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(file, "resource.azurerm_mssql_server_extended_auditing_policy", - "server_id", pattern, [""]) + assoc_au = self.get_associated_au( + file, + "resource.azurerm_mssql_server_extended_auditing_policy", + "server_id", + pattern, + [""], + ) if not assoc_au: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required resource 'azurerm_mssql_server_extended_auditing_policy' " + - f"associated to an 'azurerm_mssql_server' resource.")) - elif (element.type == "resource.azurerm_mssql_database"): + errors.append( + Error( + "sec_logging", + element, + file, + repr(element), + f"Suggestion: check for a required resource 'azurerm_mssql_server_extended_auditing_policy' " + + f"associated to an 'azurerm_mssql_server' resource.", + ) + ) + elif element.type == "resource.azurerm_mssql_database": expr = "\${azurerm_mssql_database\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(file, "resource.azurerm_mssql_database_extended_auditing_policy", - "database_id", pattern, [""]) + assoc_au = self.get_associated_au( + file, + "resource.azurerm_mssql_database_extended_auditing_policy", + "database_id", + pattern, + [""], + ) if not assoc_au: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required resource 'azurerm_mssql_database_extended_auditing_policy' " + - f"associated to an 'azurerm_mssql_database' resource.")) - elif (element.type == "resource.azurerm_postgresql_configuration"): + errors.append( + Error( + "sec_logging", + element, + file, + repr(element), + f"Suggestion: check for a required resource 'azurerm_mssql_database_extended_auditing_policy' " + + f"associated to an 'azurerm_mssql_database' resource.", + ) + ) + elif element.type == "resource.azurerm_postgresql_configuration": name = self.check_required_attribute(element.attributes, [""], "name") value = self.check_required_attribute(element.attributes, [""], "value") - if (name and name.value.lower() in ["log_connections", "connection_throttling", "log_checkpoints"] - and value and value.value.lower() != "on"): - errors.append(Error('sec_logging', value, file, repr(value))) - elif (element.type == "resource.azurerm_monitor_log_profile"): - errors.extend(self.__check_log_attribute( - element, - "categories", - file, - ["write", "delete", "action"], - all=True - )) - elif (element.type == "resource.google_sql_database_instance"): + if ( + name + and name.value.lower() + in ["log_connections", "connection_throttling", "log_checkpoints"] + and value + and value.value.lower() != "on" + ): + errors.append(Error("sec_logging", value, file, repr(value))) + elif element.type == "resource.azurerm_monitor_log_profile": + errors.extend( + self.__check_log_attribute( + element, + "categories", + file, + ["write", "delete", "action"], + all=True, + ) + ) + elif element.type == "resource.google_sql_database_instance": for flag in SecurityVisitor._GOOGLE_SQL_DATABASE_LOG_FLAGS: required_flag = True - if flag['required'] == "no": + if flag["required"] == "no": required_flag = False - errors += self.check_database_flags(element, file, 'sec_logging', flag['flag_name'], flag['value'], required_flag) - elif (element.type == "resource.azurerm_storage_container"): + errors += self.check_database_flags( + element, + file, + "sec_logging", + flag["flag_name"], + flag["value"], + required_flag, + ) + elif element.type == "resource.azurerm_storage_container": errors += self.__check_azurerm_storage_container(element, file) - elif (element.type == "resource.aws_ecs_cluster"): - name = self.check_required_attribute(element.attributes, ["setting"], "name", "containerinsights") + elif element.type == "resource.aws_ecs_cluster": + name = self.check_required_attribute( + element.attributes, ["setting"], "name", "containerinsights" + ) if name is not None: - enabled = self.check_required_attribute(element.attributes, ["setting"], "value") + enabled = self.check_required_attribute( + element.attributes, ["setting"], "value" + ) if enabled is not None: if enabled.value.lower() != "enabled": - errors.append(Error('sec_logging', enabled, file, repr(enabled))) + errors.append( + Error("sec_logging", enabled, file, repr(enabled)) + ) else: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'setting.value'.")) + errors.append( + Error( + "sec_logging", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name 'setting.value'.", + ) + ) else: - errors.append(Error('sec_logging', element, file, repr(element), - "Suggestion: check for a required attribute with name 'setting.name' and value 'containerInsights'.")) - elif (element.type == "resource.aws_vpc"): + errors.append( + Error( + "sec_logging", + element, + file, + repr(element), + "Suggestion: check for a required attribute with name 'setting.name' and value 'containerInsights'.", + ) + ) + elif element.type == "resource.aws_vpc": expr = "\${aws_vpc\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(file, "resource.aws_flow_log", - "vpc_id", pattern, [""]) + assoc_au = self.get_associated_au( + file, "resource.aws_flow_log", "vpc_id", pattern, [""] + ) if not assoc_au: - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_flow_log' " + - f"associated to an 'aws_vpc' resource.")) + errors.append( + Error( + "sec_logging", + element, + file, + repr(element), + f"Suggestion: check for a required resource 'aws_flow_log' " + + f"associated to an 'aws_vpc' resource.", + ) + ) for config in SecurityVisitor._LOGGING: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_logging', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_logging", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/missing_encryption.py b/glitch/analysis/terraform/missing_encryption.py index 0aa41256..5637b694 100644 --- a/glitch/analysis/terraform/missing_encryption.py +++ b/glitch/analysis/terraform/missing_encryption.py @@ -7,42 +7,98 @@ class TerraformMissingEncryption(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for config in SecurityVisitor._MISSING_ENCRYPTION: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - return [Error('sec_missing_encryption', attribute, file, repr(attribute))] - elif ("any_not_empty" not in config['values'] and not attribute.has_variable - and attribute.value.lower() not in config['values']): - return [Error('sec_missing_encryption', attribute, file, repr(attribute))] + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and config["values"] != [""] + ): + if ( + "any_not_empty" in config["values"] + and attribute.value.lower() == "" + ): + return [ + Error( + "sec_missing_encryption", attribute, file, repr(attribute) + ) + ] + elif ( + "any_not_empty" not in config["values"] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + ): + return [ + Error( + "sec_missing_encryption", attribute, file, repr(attribute) + ) + ] for item in SecurityVisitor._CONFIGURATION_KEYWORDS: if item.lower() == attribute.name: for config in SecurityVisitor._ENCRYPT_CONFIG: - if atomic_unit.type in config['au_type']: - expr = config['keyword'].lower() + "\s*" + config['value'].lower() + if atomic_unit.type in config["au_type"]: + expr = ( + config["keyword"].lower() + "\s*" + config["value"].lower() + ) pattern = re.compile(rf"{expr}") - if not re.search(pattern, attribute.value) and config['required'] == "yes": - return [Error('sec_missing_encryption', attribute, file, repr(attribute))] - elif re.search(pattern, attribute.value) and config['required'] == "must_not_exist": - return [Error('sec_missing_encryption', attribute, file, repr(attribute))] - + if ( + not re.search(pattern, attribute.value) + and config["required"] == "yes" + ): + return [ + Error( + "sec_missing_encryption", + attribute, + file, + repr(attribute), + ) + ] + elif ( + re.search(pattern, attribute.value) + and config["required"] == "must_not_exist" + ): + return [ + Error( + "sec_missing_encryption", + attribute, + file, + repr(attribute), + ) + ] + return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_s3_bucket"): + if element.type == "resource.aws_s3_bucket": expr = "\${aws_s3_bucket\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - r = self.get_associated_au(file, "resource.aws_s3_bucket_server_side_encryption_configuration", - "bucket", pattern, [""]) + r = self.get_associated_au( + file, + "resource.aws_s3_bucket_server_side_encryption_configuration", + "bucket", + pattern, + [""], + ) if not r: - errors.append(Error('sec_missing_encryption', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + - f"associated to an 'aws_s3_bucket' resource.")) - elif (element.type == "resource.aws_eks_cluster"): - resources = self.check_required_attribute(element.attributes, ["encryption_config"], "resources[0]") + errors.append( + Error( + "sec_missing_encryption", + element, + file, + repr(element), + f"Suggestion: check for a required resource 'aws_s3_bucket_server_side_encryption_configuration' " + + f"associated to an 'aws_s3_bucket' resource.", + ) + ) + elif element.type == "resource.aws_eks_cluster": + resources = self.check_required_attribute( + element.attributes, ["encryption_config"], "resources[0]" + ) if resources is not None: i = 0 valid = False @@ -52,36 +108,84 @@ def check(self, element, file: str): valid = True break i += 1 - resources = self.check_required_attribute(element.attributes, ["encryption_config"], f"resources[{i}]") + resources = self.check_required_attribute( + element.attributes, ["encryption_config"], f"resources[{i}]" + ) if not valid: - errors.append(Error('sec_missing_encryption', a, file, repr(a))) + errors.append(Error("sec_missing_encryption", a, file, repr(a))) else: - errors.append(Error('sec_missing_encryption', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'encryption_config.resources'.")) - elif (element.type in ["resource.aws_instance", "resource.aws_launch_configuration"]): - ebs_block_device = self.check_required_attribute(element.attributes, [""], "ebs_block_device") + errors.append( + Error( + "sec_missing_encryption", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name 'encryption_config.resources'.", + ) + ) + elif element.type in [ + "resource.aws_instance", + "resource.aws_launch_configuration", + ]: + ebs_block_device = self.check_required_attribute( + element.attributes, [""], "ebs_block_device" + ) if ebs_block_device is not None: - encrypted = self.check_required_attribute(ebs_block_device.keyvalues, [""], "encrypted") + encrypted = self.check_required_attribute( + ebs_block_device.keyvalues, [""], "encrypted" + ) if not encrypted: - errors.append(Error('sec_missing_encryption', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'ebs_block_device.encrypted'.")) - elif (element.type == "resource.aws_ecs_task_definition"): - volume = self.check_required_attribute(element.attributes, [""], "volume") + errors.append( + Error( + "sec_missing_encryption", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name 'ebs_block_device.encrypted'.", + ) + ) + elif element.type == "resource.aws_ecs_task_definition": + volume = self.check_required_attribute( + element.attributes, [""], "volume" + ) if volume is not None: - efs_volume_config = self.check_required_attribute(volume.keyvalues, [""], "efs_volume_configuration") + efs_volume_config = self.check_required_attribute( + volume.keyvalues, [""], "efs_volume_configuration" + ) if efs_volume_config is not None: - transit_encryption = self.check_required_attribute(efs_volume_config.keyvalues, [""], "transit_encryption") + transit_encryption = self.check_required_attribute( + efs_volume_config.keyvalues, [""], "transit_encryption" + ) if not transit_encryption: - errors.append(Error('sec_missing_encryption', element, file, repr(element), - f"Suggestion: check for a required attribute with name" + - f"'volume.efs_volume_configuration.transit_encryption'.")) + errors.append( + Error( + "sec_missing_encryption", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name" + + f"'volume.efs_volume_configuration.transit_encryption'.", + ) + ) for config in SecurityVisitor._MISSING_ENCRYPTION: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_missing_encryption', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_missing_encryption", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - - return errors \ No newline at end of file + + return errors diff --git a/glitch/analysis/terraform/naming.py b/glitch/analysis/terraform/naming.py index 9fcb925f..3d9f31ff 100644 --- a/glitch/analysis/terraform/naming.py +++ b/glitch/analysis/terraform/naming.py @@ -7,51 +7,115 @@ class TerraformNaming(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: - if (attribute.name == "name" and atomic_unit.type in ["resource.azurerm_storage_account"]): - pattern = r'^[a-z0-9]{3,24}$' + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: + if attribute.name == "name" and atomic_unit.type in [ + "resource.azurerm_storage_account" + ]: + pattern = r"^[a-z0-9]{3,24}$" if not re.match(pattern, attribute.value): - return [Error('sec_naming', attribute, file, repr(attribute))] + return [Error("sec_naming", attribute, file, repr(attribute))] for config in SecurityVisitor._NAMING: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - return [Error('sec_naming', attribute, file, repr(attribute))] - elif ("any_not_empty" not in config['values'] and not attribute.has_variable and - attribute.value.lower() not in config['values']): - return [Error('sec_naming', attribute, file, repr(attribute))] + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and config["values"] != [""] + ): + if ( + "any_not_empty" in config["values"] + and attribute.value.lower() == "" + ): + return [Error("sec_naming", attribute, file, repr(attribute))] + elif ( + "any_not_empty" not in config["values"] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + ): + return [Error("sec_naming", attribute, file, repr(attribute))] return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_security_group"): - ingress = self.check_required_attribute(element.attributes, [""], "ingress") - egress = self.check_required_attribute(element.attributes, [""], "egress") - if ingress and not self.check_required_attribute(ingress.keyvalues, [""], "description"): - errors.append(Error('sec_naming', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'ingress.description'.")) - if egress and not self.check_required_attribute(egress.keyvalues, [""], "description"): - errors.append(Error('sec_naming', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'egress.description'.")) - elif (element.type == "resource.google_container_cluster"): - resource_labels = self.check_required_attribute(element.attributes, [""], "resource_labels", None) + if element.type == "resource.aws_security_group": + ingress = self.check_required_attribute( + element.attributes, [""], "ingress" + ) + egress = self.check_required_attribute( + element.attributes, [""], "egress" + ) + if ingress and not self.check_required_attribute( + ingress.keyvalues, [""], "description" + ): + errors.append( + Error( + "sec_naming", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name 'ingress.description'.", + ) + ) + if egress and not self.check_required_attribute( + egress.keyvalues, [""], "description" + ): + errors.append( + Error( + "sec_naming", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name 'egress.description'.", + ) + ) + elif element.type == "resource.google_container_cluster": + resource_labels = self.check_required_attribute( + element.attributes, [""], "resource_labels", None + ) if resource_labels and resource_labels.value == None: if resource_labels.keyvalues == []: - errors.append(Error('sec_naming', resource_labels, file, repr(resource_labels), - f"Suggestion: check empty 'resource_labels'.")) + errors.append( + Error( + "sec_naming", + resource_labels, + file, + repr(resource_labels), + f"Suggestion: check empty 'resource_labels'.", + ) + ) else: - errors.append(Error('sec_naming', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'resource_labels'.")) + errors.append( + Error( + "sec_naming", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name 'resource_labels'.", + ) + ) for config in SecurityVisitor._NAMING: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_naming', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_naming", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - - return errors \ No newline at end of file + + return errors diff --git a/glitch/analysis/terraform/network_policy.py b/glitch/analysis/terraform/network_policy.py index e3f4a4d7..84d32dca 100644 --- a/glitch/analysis/terraform/network_policy.py +++ b/glitch/analysis/terraform/network_policy.py @@ -7,58 +7,140 @@ class TerraformNetworkSecurityRules(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for rule in SecurityVisitor._NETWORK_SECURITY_RULES: - if (attribute.name == rule['attribute'] and atomic_unit.type in rule['au_type'] and parent_name in rule['parents'] - and not attribute.has_variable and attribute.value is not None and - attribute.value.lower() not in rule['values'] and rule['values'] != [""]): - return [Error('sec_network_security_rules', attribute, file, repr(attribute))] + if ( + attribute.name == rule["attribute"] + and atomic_unit.type in rule["au_type"] + and parent_name in rule["parents"] + and not attribute.has_variable + and attribute.value is not None + and attribute.value.lower() not in rule["values"] + and rule["values"] != [""] + ): + return [ + Error( + "sec_network_security_rules", attribute, file, repr(attribute) + ) + ] return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): - if (element.type == "resource.azurerm_network_security_rule"): - access = self.check_required_attribute(element.attributes, [""], "access") - if (access and access.value.lower() == "allow"): - protocol = self.check_required_attribute(element.attributes, [""], "protocol") - if (protocol and protocol.value.lower() == "udp"): - errors.append(Error('sec_network_security_rules', access, file, repr(access))) - elif (protocol and protocol.value.lower() == "tcp"): - dest_port_range = self.check_required_attribute(element.attributes, [""], "destination_port_range") - port = (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]) + if element.type == "resource.azurerm_network_security_rule": + access = self.check_required_attribute( + element.attributes, [""], "access" + ) + if access and access.value.lower() == "allow": + protocol = self.check_required_attribute( + element.attributes, [""], "protocol" + ) + if protocol and protocol.value.lower() == "udp": + errors.append( + Error( + "sec_network_security_rules", access, file, repr(access) + ) + ) + elif protocol and protocol.value.lower() == "tcp": + dest_port_range = self.check_required_attribute( + element.attributes, [""], "destination_port_range" + ) + port = dest_port_range and dest_port_range.value.lower() in [ + "22", + "3389", + "*", + ] port_ranges, _ = self.iterate_required_attributes( - element.attributes, - "destination_port_ranges", - lambda x: (x.value.lower() in ["22", "3389", "*"]) + element.attributes, + "destination_port_ranges", + lambda x: (x.value.lower() in ["22", "3389", "*"]), ) if port or port_ranges: - source_address_prefix = self.check_required_attribute(element.attributes, [""], "source_address_prefix") - if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] - or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): - errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) - elif (element.type == "resource.azurerm_network_security_group"): - access = self.check_required_attribute(element.attributes, ["security_rule"], "access") - if (access and access.value.lower() == "allow"): - protocol = self.check_required_attribute(element.attributes, ["security_rule"], "protocol") - if (protocol and protocol.value.lower() == "udp"): - errors.append(Error('sec_network_security_rules', access, file, repr(access))) - elif (protocol and protocol.value.lower() == "tcp"): - dest_port_range = self.check_required_attribute(element.attributes, ["security_rule"], "destination_port_range") - if (dest_port_range and dest_port_range.value.lower() in ["22", "3389", "*"]): - source_address_prefix = self.check_required_attribute(element.attributes, [""], "source_address_prefix") - if (source_address_prefix and (source_address_prefix.value.lower() in ["*", "/0", "internet", "any"] - or re.match(r'^0.0.0.0', source_address_prefix.value.lower()))): - errors.append(Error('sec_network_security_rules', source_address_prefix, file, repr(source_address_prefix))) + source_address_prefix = self.check_required_attribute( + element.attributes, [""], "source_address_prefix" + ) + if source_address_prefix and ( + source_address_prefix.value.lower() + in ["*", "/0", "internet", "any"] + or re.match( + r"^0.0.0.0", source_address_prefix.value.lower() + ) + ): + errors.append( + Error( + "sec_network_security_rules", + source_address_prefix, + file, + repr(source_address_prefix), + ) + ) + elif element.type == "resource.azurerm_network_security_group": + access = self.check_required_attribute( + element.attributes, ["security_rule"], "access" + ) + if access and access.value.lower() == "allow": + protocol = self.check_required_attribute( + element.attributes, ["security_rule"], "protocol" + ) + if protocol and protocol.value.lower() == "udp": + errors.append( + Error( + "sec_network_security_rules", access, file, repr(access) + ) + ) + elif protocol and protocol.value.lower() == "tcp": + dest_port_range = self.check_required_attribute( + element.attributes, + ["security_rule"], + "destination_port_range", + ) + if dest_port_range and dest_port_range.value.lower() in [ + "22", + "3389", + "*", + ]: + source_address_prefix = self.check_required_attribute( + element.attributes, [""], "source_address_prefix" + ) + if source_address_prefix and ( + source_address_prefix.value.lower() + in ["*", "/0", "internet", "any"] + or re.match( + r"^0.0.0.0", source_address_prefix.value.lower() + ) + ): + errors.append( + Error( + "sec_network_security_rules", + source_address_prefix, + file, + repr(source_address_prefix), + ) + ) for rule in SecurityVisitor._NETWORK_SECURITY_RULES: - if (rule['required'] == "yes" and element.type in rule['au_type'] - and not self.check_required_attribute(element.attributes, rule['parents'], rule['attribute'])): - errors.append(Error('sec_network_security_rules', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{rule['msg']}'.")) - + if ( + rule["required"] == "yes" + and element.type in rule["au_type"] + and not self.check_required_attribute( + element.attributes, rule["parents"], rule["attribute"] + ) + ): + errors.append( + Error( + "sec_network_security_rules", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{rule['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - - return errors \ No newline at end of file + + return errors diff --git a/glitch/analysis/terraform/permission_iam_policies.py b/glitch/analysis/terraform/permission_iam_policies.py index 74849845..ebf3708e 100644 --- a/glitch/analysis/terraform/permission_iam_policies.py +++ b/glitch/analysis/terraform/permission_iam_policies.py @@ -7,34 +7,65 @@ class TerraformPermissionIAMPolicies(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: - if ((attribute.name == "member" or attribute.name.split('[')[0] == "members") + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: + if ( + (attribute.name == "member" or attribute.name.split("[")[0] == "members") and atomic_unit.type in SecurityVisitor._GOOGLE_IAM_MEMBER - and (re.search(r".-compute@developer.gserviceaccount.com", attribute.value) or - re.search(r".@appspot.gserviceaccount.com", attribute.value) or - re.search(r"user:", attribute.value))): - return [Error('sec_permission_iam_policies', attribute, file, repr(attribute))] + and ( + re.search(r".-compute@developer.gserviceaccount.com", attribute.value) + or re.search(r".@appspot.gserviceaccount.com", attribute.value) + or re.search(r"user:", attribute.value) + ) + ): + return [ + Error("sec_permission_iam_policies", attribute, file, repr(attribute)) + ] for config in SecurityVisitor._PERMISSION_IAM_POLICIES: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ((config['logic'] == "equal" and not attribute.has_variable and attribute.value.lower() not in config['values']) - or (config['logic'] == "diff" and attribute.value.lower() in config['values'])): - return [Error('sec_permission_iam_policies', attribute, file, repr(attribute))] + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and config["values"] != [""] + ): + if ( + config["logic"] == "equal" + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + ) or ( + config["logic"] == "diff" + and attribute.value.lower() in config["values"] + ): + return [ + Error( + "sec_permission_iam_policies", + attribute, + file, + repr(attribute), + ) + ] return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_iam_user"): + if element.type == "resource.aws_iam_user": expr = "\${aws_iam_user\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - assoc_au = self.get_associated_au(file, "resource.aws_iam_user_policy", "user", pattern, [""]) + assoc_au = self.get_associated_au( + file, "resource.aws_iam_user_policy", "user", pattern, [""] + ) if assoc_au is not None: - a = self.check_required_attribute(assoc_au.attributes, [""], "user", None, pattern) - errors.append(Error('sec_permission_iam_policies', a, file, repr(a))) + a = self.check_required_attribute( + assoc_au.attributes, [""], "user", None, pattern + ) + errors.append( + Error("sec_permission_iam_policies", a, file, repr(a)) + ) errors += self._check_attributes(element, file) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/public_ip.py b/glitch/analysis/terraform/public_ip.py index 37a8903b..3ff6b89a 100644 --- a/glitch/analysis/terraform/public_ip.py +++ b/glitch/analysis/terraform/public_ip.py @@ -6,28 +6,53 @@ class TerraformPublicIp(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for config in SecurityVisitor._PUBLIC_IP_CONFIGS: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and not attribute.has_variable and attribute.value is not None and - attribute.value.lower() not in config['values'] and config['values'] != [""]): - return [Error('sec_public_ip', attribute, file, repr(attribute))] - + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and not attribute.has_variable + and attribute.value is not None + and attribute.value.lower() not in config["values"] + and config["values"] != [""] + ): + return [Error("sec_public_ip", attribute, file, repr(attribute))] + return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._PUBLIC_IP_CONFIGS: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_public_ip', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif (config['required'] == "must_not_exist" and element.type in config['au_type']): - a = self.check_required_attribute(element.attributes, config['parents'], config['attribute']) + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_public_ip", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + elif ( + config["required"] == "must_not_exist" + and element.type in config["au_type"] + ): + a = self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) if a is not None: - errors.append(Error('sec_public_ip', a, file, repr(a))) - + errors.append(Error("sec_public_ip", a, file, repr(a))) + errors += self._check_attributes(element, file) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/replication.py b/glitch/analysis/terraform/replication.py index 68320567..74b9d1bf 100644 --- a/glitch/analysis/terraform/replication.py +++ b/glitch/analysis/terraform/replication.py @@ -7,33 +7,64 @@ class TerraformReplication(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for config in SecurityVisitor._REPLICATION: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""] - and not attribute.has_variable and attribute.value.lower() not in config['values']): - return [Error('sec_replication', attribute, file, repr(attribute))] - + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and config["values"] != [""] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + ): + return [Error("sec_replication", attribute, file, repr(attribute))] + return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): - if (element.type == "resource.aws_s3_bucket"): + if element.type == "resource.aws_s3_bucket": expr = "\${aws_s3_bucket\." + f"{element.name}\." pattern = re.compile(rf"{expr}") - if not self.get_associated_au(file, "resource.aws_s3_bucket_replication_configuration", - "bucket", pattern, [""]): - errors.append(Error('sec_replication', element, file, repr(element), - f"Suggestion: check for a required resource 'aws_s3_bucket_replication_configuration' " + - f"associated to an 'aws_s3_bucket' resource.")) + if not self.get_associated_au( + file, + "resource.aws_s3_bucket_replication_configuration", + "bucket", + pattern, + [""], + ): + errors.append( + Error( + "sec_replication", + element, + file, + repr(element), + f"Suggestion: check for a required resource 'aws_s3_bucket_replication_configuration' " + + f"associated to an 'aws_s3_bucket' resource.", + ) + ) for config in SecurityVisitor._REPLICATION: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_replication', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_replication", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/sensitive_iam_action.py b/glitch/analysis/terraform/sensitive_iam_action.py index 45cdc1b7..50e462f1 100644 --- a/glitch/analysis/terraform/sensitive_iam_action.py +++ b/glitch/analysis/terraform/sensitive_iam_action.py @@ -18,36 +18,54 @@ def convert_string_to_dict(input_string): if not isinstance(element, AtomicUnit): return errors - + if element.type != "data.aws_iam_policy_document": return errors - - statements = self.check_required_attribute(element.attributes, [""], "statement", return_all=True) + + statements = self.check_required_attribute( + element.attributes, [""], "statement", return_all=True + ) if statements is not None: for statement in statements: - allow = self.check_required_attribute(statement.keyvalues, [""], "effect") - if ((allow and allow.value.lower() == "allow") or (not allow)): + allow = self.check_required_attribute( + statement.keyvalues, [""], "effect" + ) + if (allow and allow.value.lower() == "allow") or (not allow): sensitive_action, action = self.iterate_required_attributes( - statement.keyvalues, - "actions", - lambda x: "*" in x.value.lower() + statement.keyvalues, "actions", lambda x: "*" in x.value.lower() ) if sensitive_action: - errors.append(Error('sec_sensitive_iam_action', action, file, repr(action))) - + errors.append( + Error( + "sec_sensitive_iam_action", action, file, repr(action) + ) + ) + wildcarded_resource, resource = self.iterate_required_attributes( - statement.keyvalues, - "resources", - lambda x: (x.value.lower() in ["*"]) or (":*" in x.value.lower()) + statement.keyvalues, + "resources", + lambda x: (x.value.lower() in ["*"]) + or (":*" in x.value.lower()), ) if wildcarded_resource: - errors.append(Error('sec_sensitive_iam_action', resource, file, repr(resource))) - elif (element.type in ["resource.aws_iam_role_policy", "resource.aws_iam_policy", - "resource.aws_iam_user_policy", "resource.aws_iam_group_policy"]): + errors.append( + Error( + "sec_sensitive_iam_action", + resource, + file, + repr(resource), + ) + ) + elif element.type in [ + "resource.aws_iam_role_policy", + "resource.aws_iam_policy", + "resource.aws_iam_user_policy", + "resource.aws_iam_group_policy", + ]: policy = self.check_required_attribute(element.attributes, [""], "policy") if policy is None: return errors - + policy_dict = convert_string_to_dict(policy.value.lower()) if not (policy_dict and policy_dict["statement"]): return errors @@ -55,27 +73,51 @@ def convert_string_to_dict(input_string): statements = policy_dict["statement"] if isinstance(statements, dict): statements = [statements] - + for statement in statements: - if not (statement["effect"] and statement["action"] and statement["resource"]): + if not ( + statement["effect"] + and statement["action"] + and statement["resource"] + ): continue if not (statement["effect"] == "allow"): continue if isinstance(statement["action"], list): for action in statement["action"]: - if ("*" in action): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + if "*" in action: + errors.append( + Error( + "sec_sensitive_iam_action", + policy, + file, + repr(policy), + ) + ) break - elif ("*" in statement["action"]): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) - + elif "*" in statement["action"]: + errors.append( + Error("sec_sensitive_iam_action", policy, file, repr(policy)) + ) + if isinstance(statement["resource"], list): for resource in statement["resource"]: if (resource in ["*"]) or (":*" in resource): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) + errors.append( + Error( + "sec_sensitive_iam_action", + policy, + file, + repr(policy), + ) + ) break - elif (statement["resource"] in ["*"]) or (":*" in statement["resource"]): - errors.append(Error('sec_sensitive_iam_action', policy, file, repr(policy))) - - return errors \ No newline at end of file + elif (statement["resource"] in ["*"]) or ( + ":*" in statement["resource"] + ): + errors.append( + Error("sec_sensitive_iam_action", policy, file, repr(policy)) + ) + + return errors diff --git a/glitch/analysis/terraform/smell_checker.py b/glitch/analysis/terraform/smell_checker.py index eefafe6b..e0f97544 100644 --- a/glitch/analysis/terraform/smell_checker.py +++ b/glitch/analysis/terraform/smell_checker.py @@ -4,77 +4,116 @@ from glitch.repr.inter import * from glitch.analysis.rules import Error, SmellChecker + class TerraformSmellChecker(SmellChecker): - def get_au(self, file: str, name: str, type: str, c = None): + def get_au(self, file: str, name: str, type: str, c=None): c = self.code if c is None else c if isinstance(c, Project): module_name = os.path.basename(os.path.dirname(file)) for m in c.modules: if m.name == module_name: - return self.get_au(file, name, type, c = m) + return self.get_au(file, name, type, c=m) elif isinstance(c, Module): for ub in c.blocks: - au = self.get_au(file, name, type, c = ub) + au = self.get_au(file, name, type, c=ub) if au is not None: return au elif isinstance(c, UnitBlock): for au in c.atomic_units: - if (au.type == type and au.name == name): + if au.type == type and au.name == name: return au return None - def get_associated_au(self, file: str, type: str, attribute_name: str, pattern, attribute_parents: list, code = None): + def get_associated_au( + self, + file: str, + type: str, + attribute_name: str, + pattern, + attribute_parents: list, + code=None, + ): code = self.code if code is None else code if isinstance(code, Project): module_name = os.path.basename(os.path.dirname(file)) for m in code.modules: if m.name == module_name: - return self.get_associated_au(file, type, attribute_name, pattern, attribute_parents, code = m) + return self.get_associated_au( + file, type, attribute_name, pattern, attribute_parents, code=m + ) elif isinstance(code, Module): for ub in code.blocks: - au = self.get_associated_au(file, type, attribute_name, pattern, attribute_parents, code = ub) + au = self.get_associated_au( + file, type, attribute_name, pattern, attribute_parents, code=ub + ) if au is not None: return au elif isinstance(code, UnitBlock): for au in code.atomic_units: - if (au.type == type and self.check_required_attribute( - au.attributes, attribute_parents, attribute_name, None, pattern)): + if au.type == type and self.check_required_attribute( + au.attributes, attribute_parents, attribute_name, None, pattern + ): return au return None - - def get_attributes_with_name_and_value(self, attributes, parents, name, value = None, pattern = None): + + def get_attributes_with_name_and_value( + self, attributes, parents, name, value=None, pattern=None + ): aux = [] for a in attributes: - if a.name.split('dynamic.')[-1] == name and parents == [""]: - if ((value and a.value.lower() == value) or (pattern and re.match(pattern, a.value.lower()))): + if a.name.split("dynamic.")[-1] == name and parents == [""]: + if (value and a.value.lower() == value) or ( + pattern and re.match(pattern, a.value.lower()) + ): aux.append(a) - elif ((value and a.value.lower() != value) or (pattern and not re.match(pattern, a.value.lower()))): + elif (value and a.value.lower() != value) or ( + pattern and not re.match(pattern, a.value.lower()) + ): continue - elif (not value and not pattern): + elif not value and not pattern: aux.append(a) - elif a.name.split('dynamic.')[-1] in parents: - aux += self.get_attributes_with_name_and_value(a.keyvalues, [""], name, value, pattern) + elif a.name.split("dynamic.")[-1] in parents: + aux += self.get_attributes_with_name_and_value( + a.keyvalues, [""], name, value, pattern + ) elif a.keyvalues != []: - aux += self.get_attributes_with_name_and_value(a.keyvalues, parents, name, value, pattern) + aux += self.get_attributes_with_name_and_value( + a.keyvalues, parents, name, value, pattern + ) return aux - def check_required_attribute(self, attributes, parents, name, value = None, pattern = None, return_all = False): - attributes = self.get_attributes_with_name_and_value(attributes, parents, name, value, pattern) + def check_required_attribute( + self, attributes, parents, name, value=None, pattern=None, return_all=False + ): + attributes = self.get_attributes_with_name_and_value( + attributes, parents, name, value, pattern + ) if attributes != []: if return_all: return attributes return attributes[0] else: return None - - def check_database_flags(self, au: AtomicUnit, file: str, smell: str, flag_name: str, safe_value: str, - required_flag = True): - database_flags = self.get_attributes_with_name_and_value(au.attributes, ["settings"], "database_flags") + + def check_database_flags( + self, + au: AtomicUnit, + file: str, + smell: str, + flag_name: str, + safe_value: str, + required_flag=True, + ): + database_flags = self.get_attributes_with_name_and_value( + au.attributes, ["settings"], "database_flags" + ) found_flag = False errors = [] if database_flags != []: for flag in database_flags: - name = self.check_required_attribute(flag.keyvalues, [""], "name", flag_name) + name = self.check_required_attribute( + flag.keyvalues, [""], "name", flag_name + ) if name is not None: found_flag = True value = self.check_required_attribute(flag.keyvalues, [""], "value") @@ -82,14 +121,28 @@ def check_database_flags(self, au: AtomicUnit, file: str, smell: str, flag_name: errors.append(Error(smell, value, file, repr(value))) break elif not value and required_flag: - errors.append(Error(smell, flag, file, repr(flag), - f"Suggestion: check for a required attribute with name 'value'.")) + errors.append( + Error( + smell, + flag, + file, + repr(flag), + f"Suggestion: check for a required attribute with name 'value'.", + ) + ) break if not found_flag and required_flag: - errors.append(Error(smell, au, file, repr(au), - f"Suggestion: check for a required flag '{flag_name}'.")) + errors.append( + Error( + smell, + au, + file, + repr(au), + f"Suggestion: check for a required flag '{flag_name}'.", + ) + ) return errors - + def iterate_required_attributes( self, attributes: List[KeyValue], name: str, check: Callable[[KeyValue], bool] ): @@ -103,19 +156,25 @@ def iterate_required_attributes( attribute = self.check_required_attribute(attributes, [""], f"{name}[{i}]") return False, None - - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: pass - def __check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def __check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: errors = [] errors += self._check_attribute(attribute, atomic_unit, parent_name, file) for attr_child in attribute.keyvalues: - errors += self.__check_attribute(attr_child, atomic_unit, attribute.name, file) + errors += self.__check_attribute( + attr_child, atomic_unit, attribute.name, file + ) return errors def _check_attributes(self, atomic_unit: AtomicUnit, file: str) -> List[Error]: errors = [] for attribute in atomic_unit.attributes: errors += self.__check_attribute(attribute, atomic_unit, "", file) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/ssl_tls_policy.py b/glitch/analysis/terraform/ssl_tls_policy.py index de9b16e4..83a9c715 100644 --- a/glitch/analysis/terraform/ssl_tls_policy.py +++ b/glitch/analysis/terraform/ssl_tls_policy.py @@ -6,32 +6,65 @@ class TerraformSslTlsPolicy(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for policy in SecurityVisitor._SSL_TLS_POLICY: - if (attribute.name == policy['attribute'] and atomic_unit.type in policy['au_type'] - and parent_name in policy['parents'] and not attribute.has_variable and attribute.value is not None and - attribute.value.lower() not in policy['values']): - return[Error('sec_ssl_tls_policy', attribute, file, repr(attribute))] - + if ( + attribute.name == policy["attribute"] + and atomic_unit.type in policy["au_type"] + and parent_name in policy["parents"] + and not attribute.has_variable + and attribute.value is not None + and attribute.value.lower() not in policy["values"] + ): + return [Error("sec_ssl_tls_policy", attribute, file, repr(attribute))] + return [] def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): - if (element.type in ["resource.aws_alb_listener", "resource.aws_lb_listener"]): - protocol = self.check_required_attribute(element.attributes, [""], "protocol") - if (protocol and protocol.value.lower() in ["https", "tls"]): - ssl_policy = self.check_required_attribute(element.attributes, [""], "ssl_policy") + if element.type in [ + "resource.aws_alb_listener", + "resource.aws_lb_listener", + ]: + protocol = self.check_required_attribute( + element.attributes, [""], "protocol" + ) + if protocol and protocol.value.lower() in ["https", "tls"]: + ssl_policy = self.check_required_attribute( + element.attributes, [""], "ssl_policy" + ) if not ssl_policy: - errors.append(Error('sec_ssl_tls_policy', element, file, repr(element), - f"Suggestion: check for a required attribute with name 'ssl_policy'.")) - + errors.append( + Error( + "sec_ssl_tls_policy", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name 'ssl_policy'.", + ) + ) + for policy in SecurityVisitor._SSL_TLS_POLICY: - if (policy['required'] == "yes" and element.type in policy['au_type'] - and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): - errors.append(Error('sec_ssl_tls_policy', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) - + if ( + policy["required"] == "yes" + and element.type in policy["au_type"] + and not self.check_required_attribute( + element.attributes, policy["parents"], policy["attribute"] + ) + ): + errors.append( + Error( + "sec_ssl_tls_policy", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/threats_detection.py b/glitch/analysis/terraform/threats_detection.py index b48a0001..7678c564 100644 --- a/glitch/analysis/terraform/threats_detection.py +++ b/glitch/analysis/terraform/threats_detection.py @@ -6,15 +6,41 @@ class TerraformThreatsDetection(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""]): - if ("any_not_empty" in config['values'] and attribute.value.lower() == ""): - return [Error('sec_threats_detection_alerts', attribute, file, repr(attribute))] - elif ("any_not_empty" not in config['values'] and not attribute.has_variable and - attribute.value.lower() not in config['values']): - return [Error('sec_threats_detection_alerts', attribute, file, repr(attribute))] + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and config["values"] != [""] + ): + if ( + "any_not_empty" in config["values"] + and attribute.value.lower() == "" + ): + return [ + Error( + "sec_threats_detection_alerts", + attribute, + file, + repr(attribute), + ) + ] + elif ( + "any_not_empty" not in config["values"] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + ): + return [ + Error( + "sec_threats_detection_alerts", + attribute, + file, + repr(attribute), + ) + ] return [] @@ -22,15 +48,34 @@ def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._MISSING_THREATS_DETECTION_ALERTS: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_threats_detection_alerts', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - elif (config['required'] == "must_not_exist" and element.type in config['au_type']): - a = self.check_required_attribute(element.attributes, config['parents'], config['attribute']) + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_threats_detection_alerts", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + elif ( + config["required"] == "must_not_exist" + and element.type in config["au_type"] + ): + a = self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) if a is not None: - errors.append(Error('sec_threats_detection_alerts', a, file, repr(a))) + errors.append( + Error("sec_threats_detection_alerts", a, file, repr(a)) + ) errors += self._check_attributes(element, file) - return errors \ No newline at end of file + return errors diff --git a/glitch/analysis/terraform/versioning.py b/glitch/analysis/terraform/versioning.py index bab470f2..5642238b 100644 --- a/glitch/analysis/terraform/versioning.py +++ b/glitch/analysis/terraform/versioning.py @@ -6,12 +6,19 @@ class TerraformVersioning(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for config in SecurityVisitor._VERSIONING: - if (attribute.name == config['attribute'] and atomic_unit.type in config['au_type'] - and parent_name in config['parents'] and config['values'] != [""] - and not attribute.has_variable and attribute.value.lower() not in config['values']): - return [Error('sec_versioning', attribute, file, repr(attribute))] + if ( + attribute.name == config["attribute"] + and atomic_unit.type in config["au_type"] + and parent_name in config["parents"] + and config["values"] != [""] + and not attribute.has_variable + and attribute.value.lower() not in config["values"] + ): + return [Error("sec_versioning", attribute, file, repr(attribute))] return [] @@ -19,11 +26,23 @@ def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for config in SecurityVisitor._VERSIONING: - if (config['required'] == "yes" and element.type in config['au_type'] - and not self.check_required_attribute(element.attributes, config['parents'], config['attribute'])): - errors.append(Error('sec_versioning', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{config['msg']}'.")) - + if ( + config["required"] == "yes" + and element.type in config["au_type"] + and not self.check_required_attribute( + element.attributes, config["parents"], config["attribute"] + ) + ): + errors.append( + Error( + "sec_versioning", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{config['msg']}'.", + ) + ) + errors += self._check_attributes(element, file) - - return errors \ No newline at end of file + + return errors diff --git a/glitch/analysis/terraform/weak_password_key_policy.py b/glitch/analysis/terraform/weak_password_key_policy.py index 532d7398..08965a91 100644 --- a/glitch/analysis/terraform/weak_password_key_policy.py +++ b/glitch/analysis/terraform/weak_password_key_policy.py @@ -6,24 +6,68 @@ class TerraformWeakPasswordKeyPolicy(TerraformSmellChecker): - def _check_attribute(self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str) -> List[Error]: + def _check_attribute( + self, attribute: Attribute, atomic_unit: AtomicUnit, parent_name: str, file: str + ) -> List[Error]: for policy in SecurityVisitor._PASSWORD_KEY_POLICY: - if (attribute.name == policy['attribute'] and atomic_unit.type in policy['au_type'] - and parent_name in policy['parents'] and policy['values'] != [""]): - if (policy['logic'] == "equal"): - if ("any_not_empty" in policy['values'] and attribute.value.lower() == ""): - return [Error('sec_weak_password_key_policy', attribute, file, repr(attribute))] - elif ("any_not_empty" not in policy['values'] and not attribute.has_variable and - attribute.value.lower() not in policy['values']): - return [Error('sec_weak_password_key_policy', attribute, file, repr(attribute))] - elif ((policy['logic'] == "gte" and not attribute.value.isnumeric()) or - (policy['logic'] == "gte" and attribute.value.isnumeric() - and int(attribute.value) < int(policy['values'][0]))): - return [Error('sec_weak_password_key_policy', attribute, file, repr(attribute))] - elif ((policy['logic'] == "lte" and not attribute.value.isnumeric()) or - (policy['logic'] == "lte" and attribute.value.isnumeric() - and int(attribute.value) > int(policy['values'][0]))): - return [Error('sec_weak_password_key_policy', attribute, file, repr(attribute))] + if ( + attribute.name == policy["attribute"] + and atomic_unit.type in policy["au_type"] + and parent_name in policy["parents"] + and policy["values"] != [""] + ): + if policy["logic"] == "equal": + if ( + "any_not_empty" in policy["values"] + and attribute.value.lower() == "" + ): + return [ + Error( + "sec_weak_password_key_policy", + attribute, + file, + repr(attribute), + ) + ] + elif ( + "any_not_empty" not in policy["values"] + and not attribute.has_variable + and attribute.value.lower() not in policy["values"] + ): + return [ + Error( + "sec_weak_password_key_policy", + attribute, + file, + repr(attribute), + ) + ] + elif (policy["logic"] == "gte" and not attribute.value.isnumeric()) or ( + policy["logic"] == "gte" + and attribute.value.isnumeric() + and int(attribute.value) < int(policy["values"][0]) + ): + return [ + Error( + "sec_weak_password_key_policy", + attribute, + file, + repr(attribute), + ) + ] + elif (policy["logic"] == "lte" and not attribute.value.isnumeric()) or ( + policy["logic"] == "lte" + and attribute.value.isnumeric() + and int(attribute.value) > int(policy["values"][0]) + ): + return [ + Error( + "sec_weak_password_key_policy", + attribute, + file, + repr(attribute), + ) + ] return [] @@ -31,11 +75,23 @@ def check(self, element, file: str): errors = [] if isinstance(element, AtomicUnit): for policy in SecurityVisitor._PASSWORD_KEY_POLICY: - if (policy['required'] == "yes" and element.type in policy['au_type'] - and not self.check_required_attribute(element.attributes, policy['parents'], policy['attribute'])): - errors.append(Error('sec_weak_password_key_policy', element, file, repr(element), - f"Suggestion: check for a required attribute with name '{policy['msg']}'.")) + if ( + policy["required"] == "yes" + and element.type in policy["au_type"] + and not self.check_required_attribute( + element.attributes, policy["parents"], policy["attribute"] + ) + ): + errors.append( + Error( + "sec_weak_password_key_policy", + element, + file, + repr(element), + f"Suggestion: check for a required attribute with name '{policy['msg']}'.", + ) + ) errors += self._check_attributes(element, file) - return errors \ No newline at end of file + return errors diff --git a/glitch/configs/default.ini b/glitch/configs/default.ini index 09658d40..f2efba19 100644 --- a/glitch/configs/default.ini +++ b/glitch/configs/default.ini @@ -28,7 +28,6 @@ secret_value_assign = [] github_actions_resources = [] file_commands = ["file", "chmod", "mkdir"] -download_commands = ["curl", "wget"] shell_resources = ["shell", "execute", "exec"] ip_binding_commands = ["ip_bind_all"] diff --git a/glitch/exceptions.py b/glitch/exceptions.py index a44dbd23..c31ae278 100644 --- a/glitch/exceptions.py +++ b/glitch/exceptions.py @@ -11,8 +11,9 @@ "DOCKER_NOT_IMPLEMENTED": "Docker - Could not parse: {}", "DOCKER_UNKNOW_ERROR": "Docker - Unknown Error: {}", "SHELL_COULD_NOT_PARSE": "Shell Command - Could not parse: {}", - "TERRAFORM_COULD_NOT_PARSE": "Terraform - Could not parse file: {}" + "TERRAFORM_COULD_NOT_PARSE": "Terraform - Could not parse file: {}", } + def throw_exception(exception, *args): print(exception.format(*args), file=sys.stderr) diff --git a/glitch/helpers.py b/glitch/helpers.py index 9735fed4..b36630bc 100644 --- a/glitch/helpers.py +++ b/glitch/helpers.py @@ -7,41 +7,41 @@ class RulesListOption(click.Option): def __init__( - self, - param_decls=None, - show_default=False, - prompt=False, - confirmation_prompt=False, - hide_input=False, is_flag=None, - flag_value=None, multiple=False, - count=False, - allow_from_autoenv=True, - type=None, help=None, - hidden=False, - show_choices=True, - show_envvar=False - ): + self, + param_decls=None, + show_default=False, + prompt=False, + confirmation_prompt=False, + hide_input=False, + is_flag=None, + flag_value=None, + multiple=False, + count=False, + allow_from_autoenv=True, + type=None, + help=None, + hidden=False, + show_choices=True, + show_envvar=False, + ): super().__init__( - param_decls = param_decls, - show_default = show_default, - prompt = prompt, - confirmation_prompt = confirmation_prompt, - hide_input = hide_input, - is_flag = is_flag, - flag_value = flag_value, - multiple = multiple, - count = count, - allow_from_autoenv = allow_from_autoenv, - type = type, - help = help, - hidden = hidden, - show_choices = show_choices, - show_envvar = show_envvar, - ) - self.type = click.Choice( - get_smell_types(), - case_sensitive=False + param_decls=param_decls, + show_default=show_default, + prompt=prompt, + confirmation_prompt=confirmation_prompt, + hide_input=hide_input, + is_flag=is_flag, + flag_value=flag_value, + multiple=multiple, + count=count, + allow_from_autoenv=allow_from_autoenv, + type=type, + help=help, + hidden=hidden, + show_choices=show_choices, + show_envvar=show_envvar, ) + self.type = click.Choice(get_smell_types(), case_sensitive=False) def get_smell_types() -> List[str]: @@ -55,11 +55,11 @@ def get_smell_types() -> List[str]: def get_smells(smell_types: List[str], tech: Tech) -> List[str]: """Get list of smells. - + Args: smell_types (List[str]): List of smell types. tech (Tech): Technology being analyzed. - + Returns: List[str]: List of smells. """ @@ -79,17 +79,16 @@ def remove_unmatched_brackets(string): stack, aux = [], "" for c in string: - if c in ["(", "[", "{"]: + if c in ["(", "[", "{"]: stack.append(c) - elif (len(stack) > 0 - and (c, stack[-1]) in [(")", "("), ("]", "["), ("}", "{")]): + elif len(stack) > 0 and (c, stack[-1]) in [(")", "("), ("]", "["), ("}", "{")]: stack.pop() - elif c in [")", "]", "}"]: + elif c in [")", "]", "}"]: continue aux += c i, res = 0, "" - while (len(stack) > 0 and i < len(aux)): + while len(stack) > 0 and i < len(aux): if aux[i] == stack[0]: stack.pop(0) continue @@ -99,56 +98,58 @@ def remove_unmatched_brackets(string): return res + # Python program for KMP Algorithm (https://www.geeksforgeeks.org/python-program-for-kmp-algorithm-for-pattern-searching-2/) # Based on code by Bhavya Jain def kmp_search(pat, txt): M = len(pat) N = len(txt) res = [] - + # create lps[] that will hold the longest prefix suffix # values for pattern lps = [0] * M - j = 0 # index for pat[] - + j = 0 # index for pat[] + # Preprocess the pattern (calculate lps[] array) compute_LPS_array(pat, M, lps) - - i = 0 # index for txt[] + + i = 0 # index for txt[] while i < N: if pat[j] == txt[i]: i += 1 j += 1 - + if j == M: res.append(i - j) - j = lps[j-1] - + j = lps[j - 1] + # mismatch after j matches elif i < N and pat[j] != txt[i]: # Do not match lps[0..lps[j-1]] characters, # they will match anyway if j != 0: - j = lps[j-1] + j = lps[j - 1] else: i += 1 return res - + + def compute_LPS_array(pat, M, lps): - len = 0 # length of the previous longest prefix suffix + len = 0 # length of the previous longest prefix suffix lps[0] i = 1 - + # the loop calculates lps[i] for i = 1 to M-1 while i < M: - if pat[i]== pat[len]: + if pat[i] == pat[len]: len += 1 lps[i] = len i += 1 else: if len != 0: - len = lps[len-1] + len = lps[len - 1] else: lps[i] = 0 - i += 1 \ No newline at end of file + i += 1 diff --git a/glitch/parsers/ansible.py b/glitch/parsers/ansible.py index a0ffe2c4..d608cfc9 100644 --- a/glitch/parsers/ansible.py +++ b/glitch/parsers/ansible.py @@ -1,7 +1,12 @@ import os import ruamel.yaml as yaml -from ruamel.yaml import ScalarNode, MappingNode, SequenceNode, \ - CommentToken, CollectionNode +from ruamel.yaml import ( + ScalarNode, + MappingNode, + SequenceNode, + CommentToken, + CollectionNode, +) from glitch.exceptions import EXCEPTIONS, throw_exception import glitch.parsers.parser as p @@ -54,13 +59,14 @@ def yaml_comments(d): c_group_comments = c_group[1].strip().split("\n") for i, comment in enumerate(c_group_comments): - if comment == "": continue + if comment == "": + continue aux = line + i comment = comment.strip() while comment not in f_lines[aux]: aux += 1 - comments.append((aux + 1, comment)) + comments.append((aux + 1, comment)) for i, line in enumerate(f_lines): if line.strip().startswith("#"): @@ -76,59 +82,73 @@ def __get_element_code(start_token, end_token, code): end_token = start_token if start_token.start_mark.line == end_token.end_mark.line: - res = code[start_token.start_mark.line][start_token.start_mark.column : end_token.end_mark.column] + res = code[start_token.start_mark.line][ + start_token.start_mark.column : end_token.end_mark.column + ] else: res = code[start_token.start_mark.line] - + for line in range(start_token.start_mark.line + 1, end_token.end_mark.line): res += code[line] - + if start_token.start_mark.line != end_token.end_mark.line: - res += code[end_token.end_mark.line][:end_token.end_mark.column] + res += code[end_token.end_mark.line][: end_token.end_mark.column] return res @staticmethod def __parse_vars(unit_block, cur_name, token, code, child=False): def create_variable(token, name, value, child=False) -> Variable: - has_variable = (("{{" in value) and ("}}" in value)) if value != None else False - if (value in ["null", "~"]): value = "" + has_variable = ( + (("{{" in value) and ("}}" in value)) if value != None else False + ) + if value in ["null", "~"]: + value = "" v = Variable(name, value, has_variable) v.line = token.start_mark.line + 1 if value == None: v.code = AnsibleParser.__get_element_code(token, token, code) else: v.code = AnsibleParser.__get_element_code(token, value, code) - v.code = ''.join(code[token.start_mark.line : token.end_mark.line + 1]) + v.code = "".join(code[token.start_mark.line : token.end_mark.line + 1]) variables.append(v) if not child: unit_block.add_variable(v) return v - variables = [] if isinstance(token, MappingNode): if cur_name == "": for key, v in token.value: if hasattr(key, "value") and isinstance(key.value, str): - AnsibleParser.__parse_vars(unit_block, key.value, v, code, child) + AnsibleParser.__parse_vars( + unit_block, key.value, v, code, child + ) elif isinstance(key.value, MappingNode): - AnsibleParser.__parse_vars(unit_block, cur_name, key.value[0][0], code, child) + AnsibleParser.__parse_vars( + unit_block, cur_name, key.value[0][0], code, child + ) else: var = create_variable(token, cur_name, None, child) for key, v in token.value: if hasattr(key, "value") and isinstance(key.value, str): - var.keyvalues += AnsibleParser.__parse_vars(unit_block, key.value, v, code, True) + var.keyvalues += AnsibleParser.__parse_vars( + unit_block, key.value, v, code, True + ) elif isinstance(key.value, MappingNode): - var.keyvalues += AnsibleParser.__parse_vars(unit_block, cur_name, key.value[0][0], code, True) + var.keyvalues += AnsibleParser.__parse_vars( + unit_block, cur_name, key.value[0][0], code, True + ) elif isinstance(token, ScalarNode): create_variable(token, cur_name, str(token.value), child) elif isinstance(token, SequenceNode): value = [] for i, val in enumerate(token.value): if isinstance(val, CollectionNode): - variables += AnsibleParser.__parse_vars(unit_block, f"{cur_name}[{i}]", val, code, child) + variables += AnsibleParser.__parse_vars( + unit_block, f"{cur_name}[{i}]", val, code, child + ) else: value.append(val.value) if len(value) > 0: @@ -139,8 +159,11 @@ def create_variable(token, name, value, child=False) -> Variable: @staticmethod def __parse_attribute(cur_name, token, val, code): def create_attribute(token, name, value) -> Attribute: - has_variable = (("{{" in value) and ("}}" in value)) if value != None else False - if (value in ["null", "~"]): value = "" + has_variable = ( + (("{{" in value) and ("}}" in value)) if value != None else False + ) + if value in ["null", "~"]: + value = "" a = Attribute(name, value, has_variable) a.line = token.start_mark.line + 1 a.column = token.start_mark.column + 1 @@ -157,8 +180,9 @@ def create_attribute(token, name, value) -> Attribute: attribute = create_attribute(token, cur_name, None) aux_attributes = [] for aux, aux_val in val.value: - aux_attributes += AnsibleParser.__parse_attribute(f"{aux.value}", - aux, aux_val, code) + aux_attributes += AnsibleParser.__parse_attribute( + f"{aux.value}", aux, aux_val, code + ) attribute.keyvalues = aux_attributes elif isinstance(val, ScalarNode): create_attribute(token, cur_name, str(val.value)) @@ -166,7 +190,9 @@ def create_attribute(token, name, value) -> Attribute: value = [] for i, v in enumerate(val.value): if not isinstance(v, ScalarNode): - attributes += AnsibleParser.__parse_attribute(f"{cur_name}[{i}]", v, v, code) + attributes += AnsibleParser.__parse_attribute( + f"{cur_name}[{i}]", v, v, code + ) else: value.append(v.value) @@ -188,7 +214,7 @@ def __parse_tasks(unit_block, tasks, code): if key.value == "include": d = Dependency(val.value) d.line = key.start_mark.line + 1 - d.code = ''.join(code[key.start_mark.line : val.end_mark.line + 1]) + d.code = "".join(code[key.start_mark.line : val.end_mark.line + 1]) unit_block.add_dependency(d) break if key.value in ["block", "always", "rescue"]: @@ -204,17 +230,22 @@ def __parse_tasks(unit_block, tasks, code): type = key.value line = task.start_mark.line + 1 - names = [n.strip() for n in name.split(',')] + names = [n.strip() for n in name.split(",")] for name in names: - if name == "": continue + if name == "": + continue atomic_units.append(AtomicUnit(name, type)) - if (isinstance(val, MappingNode)): + if isinstance(val, MappingNode): for atr, atr_val in val.value: - if (atr.value != "name"): - attributes += AnsibleParser.__parse_attribute(atr.value, atr, atr_val, code) + if atr.value != "name": + attributes += AnsibleParser.__parse_attribute( + atr.value, atr, atr_val, code + ) else: - attributes += AnsibleParser.__parse_attribute(key.value, key, val, code) + attributes += AnsibleParser.__parse_attribute( + key.value, key, val, code + ) if is_block: for au in atomic_units: @@ -227,25 +258,26 @@ def __parse_tasks(unit_block, tasks, code): au.line = line au.attributes = attributes.copy() if len(au.attributes) > 0: - au.code = ''.join(code[au.line - 1 : au.attributes[-1].line]) + au.code = "".join(code[au.line - 1 : au.attributes[-1].line]) else: au.code = code[au.line - 1] unit_block.add_atomic_unit(au) # Tasks without name - if (len(atomic_units) == 0 and type != ""): + if len(atomic_units) == 0 and type != "": au = AtomicUnit("", type) au.attributes = attributes au.line = line if len(au.attributes) > 0: - au.code = ''.join(code[au.line - 1 : au.attributes[-1].line]) + au.code = "".join(code[au.line - 1 : au.attributes[-1].line]) else: au.code = code[au.line - 1] unit_block.add_atomic_unit(au) - def __parse_playbook(self, name, file, parsed_file = None) -> UnitBlock: + def __parse_playbook(self, name, file, parsed_file=None) -> UnitBlock: try: - if parsed_file is None: parsed_file = yaml.YAML().compose(file) + if parsed_file is None: + parsed_file = yaml.YAML().compose(file) unit_block = UnitBlock(name, UnitBlockType.script) unit_block.path = file.name file.seek(0, 0) @@ -258,14 +290,16 @@ def __parse_playbook(self, name, file, parsed_file = None) -> UnitBlock: play.path = file.name for key, value in p.value: - if (key.value == "name" and play.name == ""): + if key.value == "name" and play.name == "": play.name = value.value - elif (key.value == "vars"): + elif key.value == "vars": AnsibleParser.__parse_vars(play, "", value, code) - elif (key.value in ["tasks", "pre_tasks", "post_tasks", "handlers"]): + elif key.value in ["tasks", "pre_tasks", "post_tasks", "handlers"]: AnsibleParser.__parse_tasks(play, value, code) else: - play.attributes += AnsibleParser.__parse_attribute(key.value, key, value, code) + play.attributes += AnsibleParser.__parse_attribute( + key.value, key, value, code + ) unit_block.add_unit_block(play) @@ -280,14 +314,15 @@ def __parse_playbook(self, name, file, parsed_file = None) -> UnitBlock: throw_exception(EXCEPTIONS["ANSIBLE_PLAYBOOK"], file.name) return None - def __parse_tasks_file(self, name, file, parsed_file = None) -> UnitBlock: + def __parse_tasks_file(self, name, file, parsed_file=None) -> UnitBlock: try: - if parsed_file is None: parsed_file = yaml.YAML().compose(file) + if parsed_file is None: + parsed_file = yaml.YAML().compose(file) unit_block = UnitBlock(name, UnitBlockType.tasks) unit_block.path = file.name file.seek(0, 0) code = file.readlines() - code.append("") # HACK allows to parse code in the end of the file + code.append("") # HACK allows to parse code in the end of the file if parsed_file is None: return unit_block @@ -306,7 +341,8 @@ def __parse_tasks_file(self, name, file, parsed_file = None) -> UnitBlock: def __parse_vars_file(self, name, file, parsed_file=None) -> UnitBlock: try: - if parsed_file is None: parsed_file = yaml.YAML().compose(file) + if parsed_file is None: + parsed_file = yaml.YAML().compose(file) unit_block = UnitBlock(name, UnitBlockType.vars) unit_block.path = file.name file.seek(0, 0) @@ -330,16 +366,19 @@ def __parse_vars_file(self, name, file, parsed_file=None) -> UnitBlock: @staticmethod def __apply_to_files(module, path, p_function): - if os.path.exists(path) and os.path.isdir(path) \ - and not os.path.islink(path): - files = [f for f in os.listdir(path) \ + if os.path.exists(path) and os.path.isdir(path) and not os.path.islink(path): + files = [ + f + for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) - and not f.startswith('.') and f.endswith(('.yml', '.yaml'))] + and not f.startswith(".") + and f.endswith((".yml", ".yaml")) + ] for file in files: f_path = os.path.join(path, file) with open(f_path) as f: unit_block = p_function(f_path, f) - if (unit_block is not None): + if unit_block is not None: module.add_block(unit_block) def parse_module(self, path: str) -> Module: @@ -352,41 +391,58 @@ def parse_module(self, path: str) -> Module: AnsibleParser.__apply_to_files(res, f"{path}/defaults", self.__parse_vars_file) # Check subfolders - subfolders = [f.path for f in os.scandir(f"{path}/") if f.is_dir() and not f.is_symlink()] + subfolders = [ + f.path for f in os.scandir(f"{path}/") if f.is_dir() and not f.is_symlink() + ] for d in subfolders: - if os.path.basename(os.path.normpath(d)) not \ - in ["tasks", "handlers", "vars", "defaults"]: + if os.path.basename(os.path.normpath(d)) not in [ + "tasks", + "handlers", + "vars", + "defaults", + ]: aux = self.parse_module(d) res.blocks += aux.blocks return res def parse_folder(self, path: str, root=True) -> Project: - ''' + """ It follows the sample directory layout found in: https://docs.ansible.com/ansible/latest/user_guide/sample_setup.html#sample-directory-layout - ''' + """ res: Project = Project(os.path.basename(os.path.normpath(path))) if root: AnsibleParser.__apply_to_files(res, f"{path}", self.__parse_playbook) AnsibleParser.__apply_to_files(res, f"{path}/playbooks", self.__parse_playbook) - AnsibleParser.__apply_to_files(res, f"{path}/group_vars", self.__parse_vars_file) + AnsibleParser.__apply_to_files( + res, f"{path}/group_vars", self.__parse_vars_file + ) AnsibleParser.__apply_to_files(res, f"{path}/host_vars", self.__parse_vars_file) AnsibleParser.__apply_to_files(res, f"{path}/tasks", self.__parse_tasks_file) if os.path.exists(f"{path}/roles") and not os.path.islink(f"{path}/roles"): - subfolders = [f.path for f in os.scandir(f"{path}/roles") - if f.is_dir() and not f.is_symlink()] + subfolders = [ + f.path + for f in os.scandir(f"{path}/roles") + if f.is_dir() and not f.is_symlink() + ] for m in subfolders: res.add_module(self.parse_module(m)) # Check subfolders - subfolders = [f.path for f in os.scandir(f"{path}") - if f.is_dir() and not f.is_symlink()] + subfolders = [ + f.path for f in os.scandir(f"{path}") if f.is_dir() and not f.is_symlink() + ] for d in subfolders: - if os.path.basename(os.path.normpath(d)) not \ - in ["playbooks", "group_vars", "host_vars", "tasks", "roles"]: + if os.path.basename(os.path.normpath(d)) not in [ + "playbooks", + "group_vars", + "host_vars", + "tasks", + "roles", + ]: aux = self.parse_folder(d, root=False) res.blocks += aux.blocks res.modules += aux.modules @@ -405,25 +461,31 @@ def parse_file(self, path: str, blocktype: UnitBlockType) -> UnitBlock: if blocktype == UnitBlockType.unknown: if isinstance(parsed_file, MappingNode): blocktype = UnitBlockType.vars - elif isinstance(parsed_file, SequenceNode) and len(parsed_file.value) > 0 \ - and isinstance(parsed_file.value[0], MappingNode): + elif ( + isinstance(parsed_file, SequenceNode) + and len(parsed_file.value) > 0 + and isinstance(parsed_file.value[0], MappingNode) + ): hosts = False for key in parsed_file.value[0].value: if key[0].value == "hosts": hosts = True break - + blocktype = UnitBlockType.script if hosts else UnitBlockType.tasks - elif isinstance(parsed_file, SequenceNode) and len(parsed_file.value) == 0: + elif ( + isinstance(parsed_file, SequenceNode) + and len(parsed_file.value) == 0 + ): blocktype = UnitBlockType.script else: throw_exception(EXCEPTIONS["ANSIBLE_FILE_TYPE"], path) return None - - if (blocktype == UnitBlockType.script): + + if blocktype == UnitBlockType.script: return self.__parse_playbook(path, f, parsed_file=parsed_file) - elif (blocktype == UnitBlockType.tasks): + elif blocktype == UnitBlockType.tasks: return self.__parse_tasks_file(path, f, parsed_file=parsed_file) - elif (blocktype == UnitBlockType.vars): - return self.__parse_vars_file(path, f, parsed_file=parsed_file) \ No newline at end of file + elif blocktype == UnitBlockType.vars: + return self.__parse_vars_file(path, f, parsed_file=parsed_file) diff --git a/glitch/parsers/chef.py b/glitch/parsers/chef.py index 814f1005..92f0163b 100644 --- a/glitch/parsers/chef.py +++ b/glitch/parsers/chef.py @@ -40,72 +40,103 @@ def _check_node(ast, ids, size): @staticmethod def _check_has_variable(ast): references = ["vcall", "call", "aref", "fcall", "var_ref"] - if (ChefParser._check_id(ast, ["args_add_block"])): + if ChefParser._check_id(ast, ["args_add_block"]): return ChefParser._check_id(ast.args[0][0], references) - elif(ChefParser._check_id(ast, ["method_add_arg"])): + elif ChefParser._check_id(ast, ["method_add_arg"]): return ChefParser._check_id(ast.args[0], references) - elif (ChefParser._check_id(ast, ["arg_paren"])): + elif ChefParser._check_id(ast, ["arg_paren"]): return len(ast.args) > 0 and ChefParser._check_has_variable(ast.args[0]) - elif (ChefParser._check_node(ast, ["binary"], 3)): - return ChefParser._check_has_variable(ast.args[0]) and \ - ChefParser._check_has_variable(ast.args[2]) + elif ChefParser._check_node(ast, ["binary"], 3): + return ChefParser._check_has_variable( + ast.args[0] + ) and ChefParser._check_has_variable(ast.args[2]) else: return ChefParser._check_id(ast, references) @staticmethod def _get_content_bounds(ast, source): def is_bounds(l): - return (isinstance(l, list) and len(l) == 2 and isinstance(l[0], int) - and isinstance(l[1], int)) - start_line, start_column = float('inf'), float('inf') + return ( + isinstance(l, list) + and len(l) == 2 + and isinstance(l[0], int) + and isinstance(l[1], int) + ) + + start_line, start_column = float("inf"), float("inf") end_line, end_column = 0, 0 - bounded_structures = \ - ["brace_block", "arg_paren", "string_literal", - "string_embexpr", "aref", "array", "args_add_block"] - - if (isinstance(ast, ChefParser.Node) and len(ast.args) > 0 and is_bounds(ast.args[-1])): + bounded_structures = [ + "brace_block", + "arg_paren", + "string_literal", + "string_embexpr", + "aref", + "array", + "args_add_block", + ] + + if ( + isinstance(ast, ChefParser.Node) + and len(ast.args) > 0 + and is_bounds(ast.args[-1]) + ): start_line, start_column = ast.args[-1][0], ast.args[-1][1] # The second argument counting from the end has the content # of the node (variable name, string...) - end_line, end_column = ast.args[-1][0], ast.args[-1][1] + len(ast.args[-2]) - 1 + end_line, end_column = ( + ast.args[-1][0], + ast.args[-1][1] + len(ast.args[-2]) - 1, + ) # With identifiers we need to consider the : behind them - if (ChefParser._check_id(ast, ["@ident"]) - and source[start_line - 1][start_column - 1] == ":"): + if ( + ChefParser._check_id(ast, ["@ident"]) + and source[start_line - 1][start_column - 1] == ":" + ): start_column -= 1 elif ChefParser._check_id(ast, ["@tstring_content"]): - end_line += ast.args[0].count('\\n') + end_line += ast.args[0].count("\\n") elif isinstance(ast, (list, ChefParser.Node)): for arg in ast: bound = ChefParser._get_content_bounds(arg, source) - if bound[0] < start_line: start_line = bound[0] - if bound[1] < start_column: start_column = bound[1] - if bound[2] > end_line: end_line = bound[2] - if bound[3] > end_column: end_column = bound[3] + if bound[0] < start_line: + start_line = bound[0] + if bound[1] < start_column: + start_column = bound[1] + if bound[2] > end_line: + end_line = bound[2] + if bound[3] > end_column: + end_column = bound[3] # We have to consider extra characters which correspond # to enclosing characters of these structures - if (start_line != float('inf') and ChefParser._check_id(ast, bounded_structures)): - r_brackets = ['}', ')', ']', '"', '\''] + if start_line != float("inf") and ChefParser._check_id( + ast, bounded_structures + ): + r_brackets = ["}", ")", "]", '"', "'"] # Add spaces/brackets in front of last token - for i, c in enumerate(source[end_line - 1][end_column + 1:]): + for i, c in enumerate(source[end_line - 1][end_column + 1 :]): if c in r_brackets: end_column += i + 1 break - elif not c.isspace(): break + elif not c.isspace(): + break - l_brackets = ['{', '(', '[', '"', '\''] + l_brackets = ["{", "(", "[", '"', "'"] # Add spaces/brackets behind first token for i, c in enumerate(source[start_line - 1][:start_column][::-1]): if c in l_brackets: start_column -= i + 1 break - elif not c.isspace(): break + elif not c.isspace(): + break - if (ChefParser._check_id(ast, ['string_embexpr']) - and source[start_line - 1][start_column] == "{" and - source[start_line - 1][start_column - 1] == "#"): + if ( + ChefParser._check_id(ast, ["string_embexpr"]) + and source[start_line - 1][start_column] == "{" + and source[start_line - 1][start_column - 1] == "#" + ): start_column -= 1 # The original AST does not have the start column @@ -117,48 +148,45 @@ def is_bounds(l): @staticmethod def _get_content(ast, source): - empty_structures = { - 'string_literal': "", - 'hash': "{}", - 'array': "[]" - } + empty_structures = {"string_literal": "", "hash": "{}", "array": "[]"} if isinstance(ast, list): - return ''.join(list(map(lambda a: ChefParser._get_content(a, source), ast))) + return "".join(list(map(lambda a: ChefParser._get_content(a, source), ast))) - if ((ast.id in empty_structures and len(ast.args) == 0) or - (ast.id == 'string_literal' and len(ast.args[0].args) == 0)): + if (ast.id in empty_structures and len(ast.args) == 0) or ( + ast.id == "string_literal" and len(ast.args[0].args) == 0 + ): return empty_structures[ast.id] bounds = ChefParser._get_content_bounds(ast, source) res = "" - if bounds[0] == float('inf'): + if bounds[0] == float("inf"): return res for l in range(bounds[0] - 1, bounds[2]): if bounds[0] - 1 == bounds[2] - 1: - res += source[l][bounds[1]:bounds[3] + 1] + res += source[l][bounds[1] : bounds[3] + 1] elif l == bounds[2] - 1: - res += source[l][:bounds[3] + 1] + res += source[l][: bounds[3] + 1] elif l == bounds[0] - 1: - res += source[l][bounds[1]:] + res += source[l][bounds[1] :] else: res += source[l] - if ((ast.id == "method_add_block") and (ast.args[1].id == "do_block")): + if (ast.id == "method_add_block") and (ast.args[1].id == "do_block"): res += "\nend" res = res.strip() if res.startswith(('"', "'")) and res.endswith(('"', "'")): res = res[1:-1] - + return remove_unmatched_brackets(res) @staticmethod def _get_source(ast, source): bounds = ChefParser._get_content_bounds(ast, source) - return ''.join(source[bounds[0] - 1 : bounds[2]]) + return "".join(source[bounds[0] - 1 : bounds[2]]) class Checker: def __init__(self, source): @@ -175,7 +203,7 @@ def check(self): def check_all(self): status = True - while (len(self.tests_ast_stack) != 0 and status): + while len(self.tests_ast_stack) != 0 and status: status = self.check() return status @@ -188,64 +216,90 @@ def pop(self): class ResourceChecker(Checker): def __init__(self, atomic_unit, source, ast): super().__init__(source) - self.push([self.is_block_resource, - self.is_inline_resource], ast) + self.push([self.is_block_resource, self.is_inline_resource], ast) self.atomic_unit = atomic_unit def is_block_resource(self, ast): - if (ChefParser._check_node(ast, ["method_add_block"], 2) and - ChefParser._check_node(ast.args[0], ["command"], 2) - and ChefParser._check_node(ast.args[1], ["do_block"], 1)): + if ( + ChefParser._check_node(ast, ["method_add_block"], 2) + and ChefParser._check_node(ast.args[0], ["command"], 2) + and ChefParser._check_node(ast.args[1], ["do_block"], 1) + ): self.push([self.is_resource_body], ast.args[1]) self.push([self.is_resource_def], ast.args[0]) self.atomic_unit.code = ChefParser._get_content(ast, self.source) - self.atomic_unit.line = ChefParser._get_content_bounds(ast, self.source)[0] + self.atomic_unit.line = ChefParser._get_content_bounds( + ast, self.source + )[0] return True return False def is_inline_resource(self, ast): - if (ChefParser._check_node(ast, ["command"], 2) and - ChefParser._check_id(ast.args[0], ["@ident"]) - and ChefParser._check_node(ast.args[1], ["args_add_block"], 2)): - self.push([self.is_resource_body_without_attributes, - self.is_inline_resource_name], ast.args[1]) + if ( + ChefParser._check_node(ast, ["command"], 2) + and ChefParser._check_id(ast.args[0], ["@ident"]) + and ChefParser._check_node(ast.args[1], ["args_add_block"], 2) + ): + self.push( + [ + self.is_resource_body_without_attributes, + self.is_inline_resource_name, + ], + ast.args[1], + ) self.push([self.is_resource_type], ast.args[0]) self.atomic_unit.code = ChefParser._get_content(ast, self.source) - self.atomic_unit.line = ChefParser._get_content_bounds(ast, self.source)[0] + self.atomic_unit.line = ChefParser._get_content_bounds( + ast, self.source + )[0] return True return False def is_resource_def(self, ast): - if (ChefParser._check_node(ast.args[0], ["@ident"], 2) - and ChefParser._check_node(ast.args[1], ["args_add_block"], 2)): + if ChefParser._check_node( + ast.args[0], ["@ident"], 2 + ) and ChefParser._check_node(ast.args[1], ["args_add_block"], 2): self.push([self.is_resource_name], ast.args[1]) self.push([self.is_resource_type], ast.args[0]) return True return False def is_resource_type(self, ast): - if (isinstance(ast.args[0], str) and isinstance(ast.args[1], list) \ - and not ast.args[0] in ["action", - "converge_by", - "include_recipe", - "deprecated_property_alias"]): - if ast.args[0] == "define": return False + if ( + isinstance(ast.args[0], str) + and isinstance(ast.args[1], list) + and not ast.args[0] + in [ + "action", + "converge_by", + "include_recipe", + "deprecated_property_alias", + ] + ): + if ast.args[0] == "define": + return False self.atomic_unit.type = ast.args[0] return True return False def is_resource_name(self, ast): - if (isinstance(ast.args[0][0], ChefParser.Node) and ast.args[1] is False): + if isinstance(ast.args[0][0], ChefParser.Node) and ast.args[1] is False: resource_id = ast.args[0][0] - self.atomic_unit.name = ChefParser._get_content(resource_id, self.source) + self.atomic_unit.name = ChefParser._get_content( + resource_id, self.source + ) return True return False def is_inline_resource_name(self, ast): - if (ChefParser._check_node(ast.args[0][0], ["method_add_block"], 2) - and ast.args[1] is False): + if ( + ChefParser._check_node(ast.args[0][0], ["method_add_block"], 2) + and ast.args[1] is False + ): resource_id = ast.args[0][0].args[0] - self.atomic_unit.name = ChefParser._get_content(resource_id, self.source) + self.atomic_unit.name = ChefParser._get_content( + resource_id, self.source + ) self.push([self.is_attribute], ast.args[0][0].args[1]) return True return False @@ -257,27 +311,39 @@ def is_resource_body(self, ast): return False def is_resource_body_without_attributes(self, ast): - if (ChefParser._check_id(ast.args[0][0], ["string_literal"]) and ast.args[1] is False): - self.atomic_unit.name = ChefParser._get_content(ast.args[0][0], self.source) + if ( + ChefParser._check_id(ast.args[0][0], ["string_literal"]) + and ast.args[1] is False + ): + self.atomic_unit.name = ChefParser._get_content( + ast.args[0][0], self.source + ) return True return False def is_attribute(self, ast): - if (ChefParser._check_node(ast, ["method_add_arg"], 2) - and ChefParser._check_id(ast.args[0], ["call"])): + if ChefParser._check_node( + ast, ["method_add_arg"], 2 + ) and ChefParser._check_id(ast.args[0], ["call"]): self.push([self.is_attribute], ast.args[0].args[0]) - elif ((ChefParser._check_id(ast, ["command", "method_add_arg"]) - and ast.args[1] != []) or - (ChefParser._check_id(ast, ["method_add_block"]) and - ChefParser._check_id(ast.args[0], ["method_add_arg"]) and - ChefParser._check_id(ast.args[1], ["brace_block", "do_block"]))): + elif ( + ChefParser._check_id(ast, ["command", "method_add_arg"]) + and ast.args[1] != [] + ) or ( + ChefParser._check_id(ast, ["method_add_block"]) + and ChefParser._check_id(ast.args[0], ["method_add_arg"]) + and ChefParser._check_id(ast.args[1], ["brace_block", "do_block"]) + ): has_variable = ChefParser._check_has_variable(ast.args[1]) value = ChefParser._get_content(ast.args[1], self.source) - if value == "nil": + if value == "nil": value = "" has_variable = False - a = Attribute(ChefParser._get_content(ast.args[0], self.source), - value, has_variable) + a = Attribute( + ChefParser._get_content(ast.args[0], self.source), + value, + has_variable, + ) a.line = ChefParser._get_content_bounds(ast, self.source)[0] a.code = ChefParser._get_source(ast, self.source) self.atomic_unit.add_attribute(a) @@ -296,15 +362,14 @@ def __init__(self, source, ast): def is_variable(self, ast): def create_variable(key, name, value, has_variable): variable = Variable(name, value, has_variable) - variable.line = ChefParser._get_content_bounds(key, self.source)[ - 0] - variable.code = ChefParser._get_source( - ast, self.source) + variable.line = ChefParser._get_content_bounds(key, self.source)[0] + variable.code = ChefParser._get_source(ast, self.source) return variable def parse_variable(parent, ast, key, current_name, value_ast): - if ChefParser._check_node(value_ast, ["hash"], 1) \ - and ChefParser._check_id(value_ast.args[0], ["assoclist_from_args"]): + if ChefParser._check_node( + value_ast, ["hash"], 1 + ) and ChefParser._check_id(value_ast.args[0], ["assoclist_from_args"]): variable = create_variable(key, current_name, None, False) if parent == None: self.variables.append(variable) @@ -312,9 +377,13 @@ def parse_variable(parent, ast, key, current_name, value_ast): parent.keyvalues.append(variable) parent = variable for assoc in value_ast.args[0].args[0]: - parse_variable(parent, ast, assoc.args[0], ChefParser._get_content( - assoc.args[0], self.source), - assoc.args[1]) + parse_variable( + parent, + ast, + assoc.args[0], + ChefParser._get_content(assoc.args[0], self.source), + assoc.args[1], + ) else: value = ChefParser._get_content(value_ast, self.source) has_variable = ChefParser._check_has_variable(value_ast) @@ -322,8 +391,7 @@ def parse_variable(parent, ast, key, current_name, value_ast): value = "" has_variable = False - variable = create_variable( - key, current_name, value, has_variable) + variable = create_variable(key, current_name, value, has_variable) if parent == None: self.variables.append(variable) @@ -332,14 +400,14 @@ def parse_variable(parent, ast, key, current_name, value_ast): if ChefParser._check_node(ast, ["assign"], 2): name = "" - names = ChefParser._get_content( - ast.args[0], self.source).split("[") + names = ChefParser._get_content(ast.args[0], self.source).split("[") parent = None for i, n in enumerate(names): if n.endswith("]"): n = n[:-1] - if (n.startswith("'") and n.endswith("'")) or \ - (n.startswith('"') and n.endswith('"')): + if (n.startswith("'") and n.endswith("'")) or ( + n.startswith('"') and n.endswith('"') + ): name = n[1:-1] elif n.startswith(":"): name = n[1:] @@ -347,8 +415,7 @@ def parse_variable(parent, ast, key, current_name, value_ast): name = n if i == len(names) - 1: - parse_variable( - parent, ast, ast.args[0], name, ast.args[1]) + parse_variable(parent, ast, ast.args[0], name, ast.args[1]) else: variable = create_variable(ast.args[0], name, None, False) if i == 0: @@ -367,9 +434,11 @@ def __init__(self, source, ast): self.code = "" def is_include(self, ast): - if (ChefParser._check_node(ast, ["command"], 2) - and ChefParser._check_id(ast.args[0], ["@ident"]) - and ChefParser._check_node(ast.args[1], ["args_add_block"], 2)): + if ( + ChefParser._check_node(ast, ["command"], 2) + and ChefParser._check_id(ast.args[0], ["@ident"]) + and ChefParser._check_node(ast.args[1], ["args_add_block"], 2) + ): self.push([self.is_include_name], ast.args[1]) self.push([self.is_include_type], ast.args[0]) self.code = ChefParser._get_source(ast, self.source) @@ -377,13 +446,19 @@ def is_include(self, ast): return False def is_include_type(self, ast): - if (isinstance(ast.args[0], str) and isinstance(ast.args[1], list) - and ast.args[0] == "include_recipe"): + if ( + isinstance(ast.args[0], str) + and isinstance(ast.args[1], list) + and ast.args[0] == "include_recipe" + ): return True return False def is_include_name(self, ast): - if (ChefParser._check_id(ast.args[0][0], ["string_literal"]) and ast.args[1] is False): + if ( + ChefParser._check_id(ast.args[0][0], ["string_literal"]) + and ast.args[1] is False + ): d = Dependency(ChefParser._get_content(ast.args[0][0], self.source)) d.line = ChefParser._get_content_bounds(ast, self.source)[0] d.code = self.code @@ -411,37 +486,48 @@ def is_case(self, ast): return False def is_case_condition(self, ast): - if (ChefParser._check_node(ast, ["when"], 3) \ - or ChefParser._check_node(ast, ["when"], 2)): + if ChefParser._check_node(ast, ["when"], 3) or ChefParser._check_node( + ast, ["when"], 2 + ): if self.condition is None: self.condition = ConditionalStatement( - self.case_head + " == " + ChefParser._get_content(ast.args[0][0], self.source), - ConditionalStatement.ConditionType.SWITCH + self.case_head + + " == " + + ChefParser._get_content(ast.args[0][0], self.source), + ConditionalStatement.ConditionType.SWITCH, ) self.condition.code = ChefParser._get_source(ast, self.source) - self.condition.line = ChefParser._get_content_bounds(ast, self.source)[0] + self.condition.line = ChefParser._get_content_bounds( + ast, self.source + )[0] self.current_condition = self.condition else: self.current_condition.else_statement = ConditionalStatement( - self.case_head + " == " + ChefParser._get_content(ast.args[0][0], self.source), - ConditionalStatement.ConditionType.SWITCH + self.case_head + + " == " + + ChefParser._get_content(ast.args[0][0], self.source), + ConditionalStatement.ConditionType.SWITCH, ) self.current_condition = self.current_condition.else_statement - self.current_condition.code = ChefParser._get_source(ast, self.source) - self.current_condition.line = ChefParser._get_content_bounds(ast, self.source)[0] - if (len(ast.args) == 3): + self.current_condition.code = ChefParser._get_source( + ast, self.source + ) + self.current_condition.line = ChefParser._get_content_bounds( + ast, self.source + )[0] + if len(ast.args) == 3: self.push([self.is_case_condition], ast.args[2]) return True - elif (ChefParser._check_node(ast, ["else"], 1)): + elif ChefParser._check_node(ast, ["else"], 1): self.current_condition.else_statement = ConditionalStatement( - "", - ConditionalStatement.ConditionType.SWITCH, - is_default=True + "", ConditionalStatement.ConditionType.SWITCH, is_default=True + ) + self.current_condition.else_statement.code = ChefParser._get_source( + ast, self.source + ) + self.current_condition.else_statement.line = ( + ChefParser._get_content_bounds(ast, self.source)[0] ) - self.current_condition.else_statement.code = \ - ChefParser._get_source(ast, self.source) - self.current_condition.else_statement.line = \ - ChefParser._get_content_bounds(ast, self.source)[0] return True return False @@ -455,7 +541,11 @@ def __create_ast(l): else: arg = [] for e in el: - if isinstance(e, list) and isinstance(e[0], tuple) and e[0][0] == "id": + if ( + isinstance(e, list) + and isinstance(e[0], tuple) + and e[0][0] == "id" + ): arg.append(ChefParser.__create_ast(e)) else: arg.append(e) @@ -472,7 +562,7 @@ def get_var(parent_name, vars): if var.name == parent_name: return var return None - + def add_variable_to_unit_block(variable, unit_block_vars): var_name = variable.name var = get_var(var_name, unit_block_vars) @@ -482,13 +572,14 @@ def add_variable_to_unit_block(variable, unit_block_vars): else: unit_block_vars.append(variable) - if isinstance(ast, list): for arg in ast: if isinstance(arg, (ChefParser.Node, list)): ChefParser.__transverse_ast(arg, unit_block, source) else: - resource_checker = ChefParser.ResourceChecker(AtomicUnit("", ""), source, ast) + resource_checker = ChefParser.ResourceChecker( + AtomicUnit("", ""), source, ast + ) if resource_checker.check_all(): unit_block.add_atomic_unit(resource_checker.atomic_unit) return @@ -510,7 +601,9 @@ def add_variable_to_unit_block(variable, unit_block_vars): if if_checker.check_all(): unit_block.add_statement(if_checker.condition) # Check blocks inside - ChefParser.__transverse_ast(ast.args[len(ast.args) - 1], unit_block, source) + ChefParser.__transverse_ast( + ast.args[len(ast.args) - 1], unit_block, source + ) return for arg in ast.args: @@ -520,60 +613,76 @@ def add_variable_to_unit_block(variable, unit_block_vars): @staticmethod def __parse_recipe(path, file) -> UnitBlock: with open(os.path.join(path, file)) as f: - ripper = resource_filename("glitch.parsers", 'resources/comments.rb.template') + ripper = resource_filename( + "glitch.parsers", "resources/comments.rb.template" + ) ripper = open(ripper, "r") ripper_script = Template(ripper.read()) ripper.close() - ripper_script = ripper_script.substitute({'path': '\"' + os.path.join(path, file)+ '\"'}) + ripper_script = ripper_script.substitute( + {"path": '"' + os.path.join(path, file) + '"'} + ) if "/attributes/" in path: unit_block: UnitBlock = UnitBlock(file, UnitBlockType.vars) else: unit_block: UnitBlock = UnitBlock(file, UnitBlockType.script) unit_block.path = os.path.join(path, file) - + try: source = f.readlines() except: - throw_exception(EXCEPTIONS["CHEF_COULD_NOT_PARSE"], os.path.join(path, file)) + throw_exception( + EXCEPTIONS["CHEF_COULD_NOT_PARSE"], os.path.join(path, file) + ) with tempfile.NamedTemporaryFile(mode="w+") as tmp: tmp.write(ripper_script) tmp.flush() try: - p = os.popen('ruby ' + tmp.name) + p = os.popen("ruby " + tmp.name) script_ast = p.read() p.close() comments, _ = parser_yacc(script_ast) - if comments is not None: comments.reverse() + if comments is not None: + comments.reverse() for comment, line in comments: - c = Comment(re.sub(r'\\n$', '', comment)) + c = Comment(re.sub(r"\\n$", "", comment)) c.code = source[line - 1] c.line = line unit_block.add_comment(c) except: - throw_exception(EXCEPTIONS["CHEF_COULD_NOT_PARSE"], os.path.join(path, file)) + throw_exception( + EXCEPTIONS["CHEF_COULD_NOT_PARSE"], os.path.join(path, file) + ) try: - p = os.popen('ruby -r ripper -e \'file = \ - File.open(\"' + os.path.join(path, file) + '\")\npp Ripper.sexp(file)\'') + p = os.popen( + "ruby -r ripper -e 'file = \ + File.open(\"" + + os.path.join(path, file) + + "\")\npp Ripper.sexp(file)'" + ) script_ast = p.read() p.close() _, program = parser_yacc(script_ast) ast = ChefParser.__create_ast(program) ChefParser.__transverse_ast(ast, unit_block, source) except: - throw_exception(EXCEPTIONS["CHEF_COULD_NOT_PARSE"], os.path.join(path, file)) + throw_exception( + EXCEPTIONS["CHEF_COULD_NOT_PARSE"], os.path.join(path, file) + ) return unit_block def parse_module(self, path: str) -> Module: def parse_folder(path: str): if os.path.exists(path): - files = [f for f in os.listdir(path) \ - if os.path.isfile(os.path.join(path, f))] + files = [ + f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) + ] for file in files: res.add_block(self.__parse_recipe(path, file)) @@ -597,20 +706,30 @@ def parse_folder(self, path: str) -> Project: res.add_module(self.parse_module(path)) - if (os.path.exists(f"{path}/cookbooks")): - cookbooks = [f.path for f in os.scandir(f"{path}/cookbooks") - if f.is_dir() and not f.is_symlink()] + if os.path.exists(f"{path}/cookbooks"): + cookbooks = [ + f.path + for f in os.scandir(f"{path}/cookbooks") + if f.is_dir() and not f.is_symlink() + ] for cookbook in cookbooks: res.add_module(self.parse_module(cookbook)) - subfolders = [f.path for f in os.scandir(f"{path}") - if f.is_dir() and not f.is_symlink()] + subfolders = [ + f.path for f in os.scandir(f"{path}") if f.is_dir() and not f.is_symlink() + ] for d in subfolders: - if os.path.basename(os.path.normpath(d)) not \ - in ["cookbooks", "resources", "attributes", "recipes", - "definitions", "libraries", "providers"]: + if os.path.basename(os.path.normpath(d)) not in [ + "cookbooks", + "resources", + "attributes", + "recipes", + "definitions", + "libraries", + "providers", + ]: aux = self.parse_folder(d) res.blocks += aux.blocks res.modules += aux.modules - return res \ No newline at end of file + return res diff --git a/glitch/parsers/docker.py b/glitch/parsers/docker.py index 81f2cd4c..8e850977 100644 --- a/glitch/parsers/docker.py +++ b/glitch/parsers/docker.py @@ -31,32 +31,45 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: dfp = DockerfileParser() dfp.content = f.read() structure = [ - DFPStructure( - raw_content="".join(file_lines[s['startline']:s['endline']+1]), - **s) - for s in dfp.structure] - - stage_indexes = [i for i, e in enumerate(structure) if e.instruction == 'FROM'] + DFPStructure( + raw_content="".join( + file_lines[s["startline"] : s["endline"] + 1] + ), + **s, + ) + for s in dfp.structure + ] + + stage_indexes = [ + i for i, e in enumerate(structure) if e.instruction == "FROM" + ] if len(stage_indexes) > 1: main_block = UnitBlock(os.path.basename(path), type) main_block.code = dfp.content stages = self.__get_stages(stage_indexes, structure) for i, (name, s) in enumerate(stages): - unit_block = self.__parse_stage(name, path, UnitBlockType.block, s) + unit_block = self.__parse_stage( + name, path, UnitBlockType.block, s + ) unit_block.line = structure[stage_indexes[i]].startline + 1 unit_block.code = "".join([struct.content for struct in s]) main_block.add_unit_block(unit_block) else: self.__add_user_tag(structure) - main_block = self.__parse_stage(dfp.baseimage, path, type, structure) - main_block.line = structure[stage_indexes[0]].startline + 1 \ - if stage_indexes else 1 + main_block = self.__parse_stage( + dfp.baseimage, path, type, structure + ) + main_block.line = ( + structure[stage_indexes[0]].startline + 1 + if stage_indexes + else 1 + ) main_block.code = "".join([struct.content for struct in structure]) main_block.path = path return main_block except Exception as e: - throw_exception(EXCEPTIONS['DOCKER_UNKNOW_ERROR'], e) + throw_exception(EXCEPTIONS["DOCKER_UNKNOW_ERROR"], e) main_block = UnitBlock(os.path.basename(path), type) main_block.path = path return main_block @@ -73,8 +86,16 @@ def parse_module(self, path: str) -> Module: def _parse_folder(self, path: str) -> Tuple[List[UnitBlock], List[Module]]: files = [os.path.join(path, f) for f in os.listdir(path)] - dockerfiles = [f for f in files if os.path.isfile(f) and "Dockerfile" in os.path.basename(f)] - modules = [f for f in files if os.path.isdir(f) and DockerParser._contains_dockerfiles(f)] + dockerfiles = [ + f + for f in files + if os.path.isfile(f) and "Dockerfile" in os.path.basename(f) + ] + modules = [ + f + for f in files + if os.path.isdir(f) and DockerParser._contains_dockerfiles(f) + ] blocks = [self.parse_file(f, UnitBlockType.script) for f in dockerfiles] modules = [self.parse_module(f) for f in modules] @@ -87,50 +108,55 @@ def _contains_dockerfiles(path: str) -> bool: if not os.path.isdir(path): return "Dockerfile" in os.path.basename(path) for f in os.listdir(path): - contains_dockerfiles = DockerParser._contains_dockerfiles(os.path.join(path, f)) + contains_dockerfiles = DockerParser._contains_dockerfiles( + os.path.join(path, f) + ) if contains_dockerfiles: return True return False @staticmethod - def __parse_stage(name: str, path: str, unit_type: UnitBlockType, structure: List[DFPStructure]) -> UnitBlock: + def __parse_stage( + name: str, path: str, unit_type: UnitBlockType, structure: List[DFPStructure] + ) -> UnitBlock: u = UnitBlock(name, unit_type) u.path = path for s in structure: try: DockerParser.__parse_instruction(s, u) except NotImplementedError: - throw_exception(EXCEPTIONS['DOCKER_NOT_IMPLEMENTED'], s.content) + throw_exception(EXCEPTIONS["DOCKER_NOT_IMPLEMENTED"], s.content) return u @staticmethod def __parse_instruction(element: DFPStructure, unit_block: UnitBlock): instruction = element.instruction - if instruction in ['ENV', 'USER', 'ARG', 'LABEL']: + if instruction in ["ENV", "USER", "ARG", "LABEL"]: unit_block.variables += DockerParser.__create_variable_block(element) - elif instruction == 'COMMENT': + elif instruction == "COMMENT": c = Comment(element.value) c.line = element.startline + 1 c.code = element.content unit_block.add_comment(c) - elif instruction in ['RUN', 'CMD', 'ENTRYPOINT']: + elif instruction in ["RUN", "CMD", "ENTRYPOINT"]: try: c_parser = CommandParser(element) aus = c_parser.parse_command() unit_block.atomic_units += aus except Exception: - throw_exception(EXCEPTIONS['SHELL_COULD_NOT_PARSE'], element.content) - elif instruction == 'ONBUILD': + throw_exception(EXCEPTIONS["SHELL_COULD_NOT_PARSE"], element.content) + elif instruction == "ONBUILD": dfp = DockerfileParser() dfp.content = element.value - element = DFPStructure(**dfp.structure[0], - raw_content=dfp.structure[0]['content']) + element = DFPStructure( + **dfp.structure[0], raw_content=dfp.structure[0]["content"] + ) DockerParser.__parse_instruction(element, unit_block) - elif instruction == 'COPY': + elif instruction == "COPY": au = AtomicUnit("", "copy") paths = [v for v in element.value.split(" ") if not v.startswith("--")] - au.add_attribute(Attribute('src', str(paths[0:-1]), False)) - au.add_attribute(Attribute('dest', paths[-1], False)) + au.add_attribute(Attribute("src", str(paths[0:-1]), False)) + au.add_attribute(Attribute("dest", paths[-1], False)) for attr in au.attributes: attr.code = element.content attr.line = element.startline + 1 @@ -138,25 +164,38 @@ def __parse_instruction(element: DFPStructure, unit_block: UnitBlock): au.line = element.startline + 1 unit_block.add_atomic_unit(au) # TODO: Investigate keywords and parse them - elif instruction in ['ADD', 'VOLUME', 'WORKDIR']: + elif instruction in ["ADD", "VOLUME", "WORKDIR"]: pass - elif instruction in ['STOPSIGNAL', 'HEALTHCHECK', 'SHELL']: + elif instruction in ["STOPSIGNAL", "HEALTHCHECK", "SHELL"]: pass - elif instruction == 'EXPOSE': + elif instruction == "EXPOSE": pass @staticmethod - def __get_stages(stage_indexes: List[int], structure: List[DFPStructure]) -> List[Tuple[str, List[DFPStructure]]]: + def __get_stages( + stage_indexes: List[int], structure: List[DFPStructure] + ) -> List[Tuple[str, List[DFPStructure]]]: stages = [] for i, stage_i in enumerate(stage_indexes): stage_image = structure[stage_i].value.split(" ")[0] stage_start = stage_i if i != 0 else 0 - stage_end = len(structure) if i == len(stage_indexes) - 1 else stage_indexes[i + 1] - stages.append((stage_image, DockerParser.__get_stage_structure(structure, stage_start, stage_end))) + stage_end = ( + len(structure) if i == len(stage_indexes) - 1 else stage_indexes[i + 1] + ) + stages.append( + ( + stage_image, + DockerParser.__get_stage_structure( + structure, stage_start, stage_end + ), + ) + ) return stages @staticmethod - def __get_stage_structure(structure: List[DFPStructure], stage_start: int, stage_end: int): + def __get_stage_structure( + structure: List[DFPStructure], stage_start: int, stage_end: int + ): sub_structure = structure[stage_start:stage_end].copy() DockerParser.__add_user_tag(sub_structure) return sub_structure @@ -164,37 +203,45 @@ def __get_stage_structure(structure: List[DFPStructure], stage_start: int, stage @staticmethod def __create_variable_block(element: DFPStructure) -> List[Variable]: variables = [] - if element.instruction == 'USER': + if element.instruction == "USER": variables.append(Variable("user-profile", element.value, False)) - elif element.instruction == 'ARG': + elif element.instruction == "ARG": value = element.value.split("=") arg = value[0] default = value[1] if len(value) == 2 else None variables.append(Variable(arg, default if default else "ARG", True)) - elif element.instruction == 'ENV': + elif element.instruction == "ENV": if "=" in element.value: # TODO: Improve code attribution for multiple values - return DockerParser.__parse_multiple_key_value_variables(element.content, element.startline) + return DockerParser.__parse_multiple_key_value_variables( + element.content, element.startline + ) if len(element.value.split(" ")) != 2: raise NotImplementedError() env, value = element.value.split(" ") variables.append(Variable(env, value, value.startswith("$"))) else: # LABEL - return DockerParser.__parse_multiple_key_value_variables(element.content, element.startline) + return DockerParser.__parse_multiple_key_value_variables( + element.content, element.startline + ) for v in variables: - if v.value == "\"\"" or v.value == "''": + if v.value == '""' or v.value == "''": v.value = "" v.line = element.startline + 1 v.code = element.content return variables @staticmethod - def __parse_multiple_key_value_variables(content: str, base_line: int) -> List[Variable]: + def __parse_multiple_key_value_variables( + content: str, base_line: int + ) -> List[Variable]: variables = [] - for i, line in enumerate(content.split('\n')): - for match in re.finditer(r"([\w_]*)=(?:(?:'|\")([\w\. <>@]*)(?:\"|')|([\w\.]*))", line): - value = match.group(2) or match.group(3) or '' + for i, line in enumerate(content.split("\n")): + for match in re.finditer( + r"([\w_]*)=(?:(?:'|\")([\w\. <>@]*)(?:\"|')|([\w\.]*))", line + ): + value = match.group(2) or match.group(3) or "" v = Variable(match.group(1), value, value.startswith("$")) v.line = base_line + i + 1 v.code = line @@ -212,12 +259,13 @@ def __add_user_tag(structure: List[DFPStructure]): index, line = -1, 0 for i, s in enumerate(structure): - if s.instruction == 'FROM': + if s.instruction == "FROM": index = i line = s.startline break - structure.insert(index + 1, DFPStructure( - "USER root", line, "USER", line, "root", "")) + structure.insert( + index + 1, DFPStructure("USER root", line, "USER", line, "root", "") + ) @dataclass @@ -226,7 +274,9 @@ class ShellCommand: command: str args: List[str] code: str - options: Dict[str, Tuple[Union[str, bool, int, float], str]] = field(default_factory=dict) + options: Dict[str, Tuple[Union[str, bool, int, float], str]] = field( + default_factory=dict + ) main_arg: Optional[str] = None line: int = -1 @@ -249,8 +299,12 @@ def to_atomic_unit(self) -> AtomicUnit: class CommandParser: - def __init__(self, command: DFPStructure): - value = command.content.replace("RUN ", "") if command.instruction == "RUN" else command.value + def __init__(self, command: DFPStructure): + value = ( + command.content.replace("RUN ", "") + if command.instruction == "RUN" + else command.value + ) if value.startswith("[") and value.endswith("]"): c_list = ast.literal_eval(value) value = " ".join(c_list) @@ -266,47 +320,53 @@ def parse_command(self) -> List[AtomicUnit]: try: aus.append(self.__parse_single_command(c, line)) except IndexError: - throw_exception(EXCEPTIONS['SHELL_COULD_NOT_PARSE'].format(" ".join(c))) + throw_exception(EXCEPTIONS["SHELL_COULD_NOT_PARSE"].format(" ".join(c))) return aus def __parse_single_command(self, command: List[str], line: int) -> AtomicUnit: - command, carriage_returns = CommandParser.__strip_shell_command( - command - ) + command, carriage_returns = CommandParser.__strip_shell_command(command) line += carriage_returns sudo = command[0] == "sudo" name_index = 0 if not sudo else 1 command_type = command[name_index] if len(command) == name_index + 1: return ShellCommand( - sudo=sudo, command=command_type, args=[], main_arg=command_type, line=line, code=self.dfp_structure.raw_content + sudo=sudo, + command=command_type, + args=[], + main_arg=command_type, + line=line, + code=self.dfp_structure.raw_content, ).to_atomic_unit() c = ShellCommand( - sudo=sudo, command=command_type, args=command[name_index + 1:], line=line, code=self.dfp_structure.raw_content + sudo=sudo, + command=command_type, + args=command[name_index + 1 :], + line=line, + code=self.dfp_structure.raw_content, ) CommandParser.__parse_shell_command(c) return c.to_atomic_unit() @staticmethod def __strip_shell_command(command: List[str]) -> Tuple[List[str], int]: - non_empty_indexes = [i for i, c in enumerate(command) - if c not in ['\n', '', ' ', '\r']] + non_empty_indexes = [ + i for i, c in enumerate(command) if c not in ["\n", "", " ", "\r"] + ] if not non_empty_indexes: return [] start, end = non_empty_indexes[0], non_empty_indexes[-1] - return command[start:end+1], sum(1 for c in command if c == "\n") + return command[start : end + 1], sum(1 for c in command if c == "\n") @staticmethod def __parse_shell_command(command: ShellCommand): if command.command == "chmod": - reference = [arg for arg in command.args if '--reference' in arg] - command.args = [arg for arg in command.args - if not arg.startswith('-')] + reference = [arg for arg in command.args if "--reference" in arg] + command.args = [arg for arg in command.args if not arg.startswith("-")] command.main_arg = command.args[-1] if reference: reference[0] - command.options['reference'] = (reference.split('=')[1], - reference) + command.options["reference"] = (reference.split("=")[1], reference) else: command.options["mode"] = command.args[0], command.args[0] else: @@ -317,9 +377,7 @@ def __parse_general_command(command: ShellCommand): args = command.args.copy() # TODO: Solve issue where last argument is part of a parameter main_arg_index = -1 if not args[-1].startswith("-") else 0 - if len(args) >= 3 and \ - args[-2].startswith('-') and \ - not args[0].startswith('-'): + if len(args) >= 3 and args[-2].startswith("-") and not args[0].startswith("-"): main_arg_index = 0 main_arg = args[main_arg_index] del args[main_arg_index] @@ -344,11 +402,15 @@ def __parse_general_command(command: ShellCommand): def __get_sub_commands(self) -> List[Tuple[int, List[str]]]: commands = [] tmp = [] - lines = self.command.split('\n') if not self.__contains_multi_line_values(self.command) else [self.command] + lines = ( + self.command.split("\n") + if not self.__contains_multi_line_values(self.command) + else [self.command] + ) current_line = self.line for i, line in enumerate(lines): for part in bashlex.split(line): - if part in ['&&', '&', '|', ';']: + if part in ["&&", "&", "|", ";"]: commands.append((current_line, tmp)) current_line = self.line + i tmp = [] @@ -360,11 +422,16 @@ def __get_sub_commands(self) -> List[Tuple[int, List[str]]]: @staticmethod def __contains_multi_line_values(command: str) -> bool: def is_multi_line_str(line: str) -> bool: - return line.count("\"") % 2 != 0 or line.count("'") % 2 != 0 + return line.count('"') % 2 != 0 or line.count("'") % 2 != 0 def has_open_parentheses(line: str) -> bool: - return line.count("(") != line.count(")") or line.count("[") != line.count("]") \ - or line.count("{") != line.count("}") + return ( + line.count("(") != line.count(")") + or line.count("[") != line.count("]") + or line.count("{") != line.count("}") + ) lines = command.split("\n") - return any((is_multi_line_str(line) or has_open_parentheses(line)) for line in lines) + return any( + (is_multi_line_str(line) or has_open_parentheses(line)) for line in lines + ) diff --git a/glitch/parsers/parser.py b/glitch/parsers/parser.py index 945ec87d..810db843 100644 --- a/glitch/parsers/parser.py +++ b/glitch/parsers/parser.py @@ -4,6 +4,7 @@ from glitch.repr.inter import UnitBlockType + class Parser(ABC): def parse(self, path: str, type: UnitBlockType, is_module: bool) -> Module: if is_module: @@ -34,4 +35,4 @@ def parse_file_structure(self, folder, path): elif os.path.isdir(os.path.join(path, f)): new_folder = Folder(f) self.parse_file_structure(new_folder, os.path.join(path, f)) - folder.add_folder(new_folder) \ No newline at end of file + folder.add_folder(new_folder) diff --git a/glitch/parsers/puppet.py b/glitch/parsers/puppet.py index d8a4abc6..02684254 100644 --- a/glitch/parsers/puppet.py +++ b/glitch/parsers/puppet.py @@ -16,7 +16,7 @@ def get_var(parent_name, vars): if var.name == parent_name: return var return None - + def add_variable_to_unit_block(variable, unit_block_vars): var_name = variable.name var = get_var(var_name, unit_block_vars) @@ -25,7 +25,7 @@ def add_variable_to_unit_block(variable, unit_block_vars): add_variable_to_unit_block(v, var.keyvalues) else: unit_block_vars.append(variable) - + if isinstance(ce, Dependency): unit_block.add_dependency(ce) elif isinstance(ce, Variable): @@ -49,50 +49,55 @@ def get_code(ce): res = code[ce.line - 1][max(0, ce.col - 1) : ce.end_col - 1] else: res = code[ce.line - 1] - + for line in range(ce.line, ce.end_line - 1): res += code[line] - + if ce.line != ce.end_line: - res += code[ce.end_line - 1][:ce.end_col - 1] + res += code[ce.end_line - 1][: ce.end_col - 1] return res - + def process_hash_value(name: str, temp_value): - if '[' in name and ']' in name: - start = name.find('[') + 1 - end = name.find(']') + if "[" in name and "]" in name: + start = name.find("[") + 1 + end = name.find("]") key_name = name[start:end] - name_without_key = name[:start-1] + name[end+1:] + name_without_key = name[: start - 1] + name[end + 1 :] n, d = process_hash_value(name_without_key, temp_value) if d == {}: d[key_name] = temp_value return n, d else: - new_d : dict = {} + new_d: dict = {} new_d[key_name] = d return n, new_d else: return name, {} - - if (isinstance(codeelement, puppetmodel.Value)): + + if isinstance(codeelement, puppetmodel.Value): if isinstance(codeelement, puppetmodel.Hash): res = {} for key, value in codeelement.value.items(): - res[PuppetParser.__process_codeelement(key, path, code)] = \ - PuppetParser.__process_codeelement(value, path, code) + res[ + PuppetParser.__process_codeelement(key, path, code) + ] = PuppetParser.__process_codeelement(value, path, code) return res elif isinstance(codeelement, puppetmodel.Array): - return str(PuppetParser.__process_codeelement(codeelement.value, path, code)) + return str( + PuppetParser.__process_codeelement(codeelement.value, path, code) + ) elif codeelement.value == None: return "" return str(codeelement.value) - elif (isinstance(codeelement, puppetmodel.Attribute)): + elif isinstance(codeelement, puppetmodel.Attribute): name = PuppetParser.__process_codeelement(codeelement.key, path, code) if codeelement.value is not None: - temp_value = PuppetParser.__process_codeelement(codeelement.value, path, code) + temp_value = PuppetParser.__process_codeelement( + codeelement.value, path, code + ) value = "" if temp_value == "undef" else temp_value else: value = None @@ -101,66 +106,80 @@ def process_hash_value(name: str, temp_value): attribute.line, attribute.column = codeelement.line, codeelement.col attribute.code = get_code(codeelement) return attribute - elif (isinstance(codeelement, puppetmodel.Resource)): + elif isinstance(codeelement, puppetmodel.Resource): resource: AtomicUnit = AtomicUnit( - PuppetParser.__process_codeelement(codeelement.title, path, code), - PuppetParser.__process_codeelement(codeelement.type, path, code) + PuppetParser.__process_codeelement(codeelement.title, path, code), + PuppetParser.__process_codeelement(codeelement.type, path, code), ) for attr in codeelement.attributes: - resource.add_attribute(PuppetParser.__process_codeelement(attr, path, code)) + resource.add_attribute( + PuppetParser.__process_codeelement(attr, path, code) + ) resource.line, resource.column = codeelement.line, codeelement.col resource.code = get_code(codeelement) - return resource - elif (isinstance(codeelement, puppetmodel.ClassAsResource)): + return resource + elif isinstance(codeelement, puppetmodel.ClassAsResource): resource: AtomicUnit = AtomicUnit( - PuppetParser.__process_codeelement(codeelement.title, path, code), - "class" + PuppetParser.__process_codeelement(codeelement.title, path, code), + "class", ) for attr in codeelement.attributes: - resource.add_attribute(PuppetParser.__process_codeelement(attr, path, code)) + resource.add_attribute( + PuppetParser.__process_codeelement(attr, path, code) + ) resource.line, resource.column = codeelement.line, codeelement.col resource.code = get_code(codeelement) - return resource - elif (isinstance(codeelement, puppetmodel.ResourceDeclaration)): + return resource + elif isinstance(codeelement, puppetmodel.ResourceDeclaration): unit_block: UnitBlock = UnitBlock( PuppetParser.__process_codeelement(codeelement.name, path, code), - UnitBlockType.block + UnitBlockType.block, ) unit_block.path = path - if (codeelement.block is not None): - for ce in list(map(lambda ce: PuppetParser.__process_codeelement(ce, path, code), codeelement.block)): + if codeelement.block is not None: + for ce in list( + map( + lambda ce: PuppetParser.__process_codeelement(ce, path, code), + codeelement.block, + ) + ): PuppetParser.__process_unitblock_component(ce, unit_block) for p in codeelement.parameters: - unit_block.add_attribute(PuppetParser.__process_codeelement(p, path, code)) + unit_block.add_attribute( + PuppetParser.__process_codeelement(p, path, code) + ) unit_block.line, unit_block.column = codeelement.line, codeelement.col unit_block.code = get_code(codeelement) return unit_block - elif (isinstance(codeelement, puppetmodel.Parameter)): + elif isinstance(codeelement, puppetmodel.Parameter): # FIXME Parameters are not yet supported name = PuppetParser.__process_codeelement(codeelement.name, path, code) if codeelement.default is not None: - temp_value = PuppetParser.__process_codeelement(codeelement.default, path, code) + temp_value = PuppetParser.__process_codeelement( + codeelement.default, path, code + ) value = "" if temp_value == "undef" else temp_value else: value = None - has_variable = not isinstance(value, str) or temp_value.startswith("$") or \ - codeelement.default is None - attribute = Attribute( - name, - value, - has_variable + has_variable = ( + not isinstance(value, str) + or temp_value.startswith("$") + or codeelement.default is None ) + attribute = Attribute(name, value, has_variable) attribute.line, attribute.column = codeelement.line, codeelement.col attribute.code = get_code(codeelement) return attribute - elif (isinstance(codeelement, puppetmodel.Assignment)): + elif isinstance(codeelement, puppetmodel.Assignment): name = PuppetParser.__process_codeelement(codeelement.name, path, code) - temp_value = PuppetParser.__process_codeelement(codeelement.value, path, code) - if '[' in name and ']' in name: + temp_value = PuppetParser.__process_codeelement( + codeelement.value, path, code + ) + if "[" in name and "]" in name: name, temp_value = process_hash_value(name, temp_value) if not isinstance(temp_value, dict): if codeelement.value is not None: @@ -175,71 +194,128 @@ def process_hash_value(name: str, temp_value): else: variable: Variable = Variable(name, None, False) variable.line, variable.column = codeelement.line, codeelement.col - variable.code = get_code(codeelement) + variable.code = get_code(codeelement) for key, value in temp_value.items(): - variable.keyvalues.append(PuppetParser.__process_codeelement( - puppetmodel.Assignment(codeelement.line, codeelement.col, - codeelement.end_line, codeelement.end_col, key, value), path, code)) - + variable.keyvalues.append( + PuppetParser.__process_codeelement( + puppetmodel.Assignment( + codeelement.line, + codeelement.col, + codeelement.end_line, + codeelement.end_col, + key, + value, + ), + path, + code, + ) + ) + return variable - elif (isinstance(codeelement, puppetmodel.PuppetClass)): + elif isinstance(codeelement, puppetmodel.PuppetClass): # FIXME there are components of the class that are not considered unit_block: UnitBlock = UnitBlock( PuppetParser.__process_codeelement(codeelement.name, path, code), - UnitBlockType.block + UnitBlockType.block, ) unit_block.path = path - if (codeelement.block is not None): - for ce in list(map(lambda ce: PuppetParser.__process_codeelement(ce, path, code), codeelement.block)): + if codeelement.block is not None: + for ce in list( + map( + lambda ce: PuppetParser.__process_codeelement(ce, path, code), + codeelement.block, + ) + ): PuppetParser.__process_unitblock_component(ce, unit_block) for p in codeelement.parameters: - unit_block.add_attribute(PuppetParser.__process_codeelement(p, path, code)) + unit_block.add_attribute( + PuppetParser.__process_codeelement(p, path, code) + ) unit_block.line, unit_block.column = codeelement.line, codeelement.col unit_block.code = get_code(codeelement) return unit_block - elif (isinstance(codeelement, puppetmodel.Node)): + elif isinstance(codeelement, puppetmodel.Node): # FIXME Nodes are not yet supported - if (codeelement.block is not None): - return list(map(lambda ce: PuppetParser.__process_codeelement(ce, path, code), codeelement.block)) + if codeelement.block is not None: + return list( + map( + lambda ce: PuppetParser.__process_codeelement(ce, path, code), + codeelement.block, + ) + ) else: return [] - elif (isinstance(codeelement, puppetmodel.Operation)): + elif isinstance(codeelement, puppetmodel.Operation): if len(codeelement.arguments) == 1: - return codeelement.operator + \ - PuppetParser.__process_codeelement(codeelement.arguments[0], path, code) + return codeelement.operator + PuppetParser.__process_codeelement( + codeelement.arguments[0], path, code + ) elif codeelement.operator == "[]": - return \ - (PuppetParser.__process_codeelement(codeelement.arguments[0], path, code) - + "[" + - ','.join(PuppetParser.__process_codeelement(codeelement.arguments[1], path, code)) - + "]") + return ( + PuppetParser.__process_codeelement( + codeelement.arguments[0], path, code + ) + + "[" + + ",".join( + PuppetParser.__process_codeelement( + codeelement.arguments[1], path, code + ) + ) + + "]" + ) elif len(codeelement.arguments) == 2: - return \ - (str(PuppetParser.__process_codeelement(codeelement.arguments[0], path, code)) - + codeelement.operator + - str(PuppetParser.__process_codeelement(codeelement.arguments[1], path, code))) + return ( + str( + PuppetParser.__process_codeelement( + codeelement.arguments[0], path, code + ) + ) + + codeelement.operator + + str( + PuppetParser.__process_codeelement( + codeelement.arguments[1], path, code + ) + ) + ) elif codeelement.operator == "[,]": - return \ - (PuppetParser.__process_codeelement(codeelement.arguments[0], path, code) - + "[" + - PuppetParser.__process_codeelement(codeelement.arguments[1], path, code) - + "," + - PuppetParser.__process_codeelement(codeelement.arguments[2], path, code) - + "]") - elif (isinstance(codeelement, puppetmodel.Lambda)): + return ( + PuppetParser.__process_codeelement( + codeelement.arguments[0], path, code + ) + + "[" + + PuppetParser.__process_codeelement( + codeelement.arguments[1], path, code + ) + + "," + + PuppetParser.__process_codeelement( + codeelement.arguments[2], path, code + ) + + "]" + ) + elif isinstance(codeelement, puppetmodel.Lambda): # FIXME Lambdas are not yet supported - if (codeelement.block is not None): + if codeelement.block is not None: args = [] for arg in codeelement.parameters: attr = PuppetParser.__process_codeelement(arg, path, code) args.append(Variable(attr.name, "", True)) - return list(map(lambda ce: PuppetParser.__process_codeelement(ce, path, code), codeelement.block)) + args + return ( + list( + map( + lambda ce: PuppetParser.__process_codeelement( + ce, path, code + ), + codeelement.block, + ) + ) + + args + ) else: return [] - elif (isinstance(codeelement, puppetmodel.FunctionCall)): + elif isinstance(codeelement, puppetmodel.FunctionCall): # FIXME Function calls are not yet supported res = PuppetParser.__process_codeelement(codeelement.name, path, code) + "(" for arg in codeelement.arguments: @@ -247,9 +323,11 @@ def process_hash_value(name: str, temp_value): res = res[:-1] res += ")" lamb = PuppetParser.__process_codeelement(codeelement.lamb, path, code) - if lamb != "": return [res] + lamb - else: return res - elif (isinstance(codeelement, puppetmodel.If)): + if lamb != "": + return [res] + lamb + else: + return res + elif isinstance(codeelement, puppetmodel.If): condition = PuppetParser.__process_codeelement( codeelement.condition, path, code ) @@ -258,26 +336,32 @@ def process_hash_value(name: str, temp_value): ) body = list( map( - lambda ce: PuppetParser.__process_codeelement(ce, path, code), - codeelement.block + lambda ce: PuppetParser.__process_codeelement(ce, path, code), + codeelement.block, ) ) for statement in body: condition.add_statement(statement) - - if (codeelement.elseblock is not None): + + if codeelement.elseblock is not None: condition.else_statement = PuppetParser.__process_codeelement( codeelement.elseblock, path, code ) return condition - elif (isinstance(codeelement, puppetmodel.Unless)): + elif isinstance(codeelement, puppetmodel.Unless): # FIXME Unless is not yet supported - res = list(map(lambda ce: PuppetParser.__process_codeelement(ce, path, code), - codeelement.block)) - if (codeelement.elseblock is not None): - res += PuppetParser.__process_codeelement(codeelement.elseblock, path, code) + res = list( + map( + lambda ce: PuppetParser.__process_codeelement(ce, path, code), + codeelement.block, + ) + ) + if codeelement.elseblock is not None: + res += PuppetParser.__process_codeelement( + codeelement.elseblock, path, code + ) return res - elif (isinstance(codeelement, puppetmodel.Include)): + elif isinstance(codeelement, puppetmodel.Include): dependencies = [] for inc in codeelement.inc: d = Dependency(PuppetParser.__process_codeelement(inc, path, code)) @@ -285,7 +369,7 @@ def process_hash_value(name: str, temp_value): d.code = get_code(codeelement) dependencies.append(d) return dependencies - elif (isinstance(codeelement, puppetmodel.Require)): + elif isinstance(codeelement, puppetmodel.Require): dependencies = [] for req in codeelement.req: d = Dependency(PuppetParser.__process_codeelement(req, path, code)) @@ -293,7 +377,7 @@ def process_hash_value(name: str, temp_value): d.code = get_code(codeelement) dependencies.append(d) return dependencies - elif (isinstance(codeelement, puppetmodel.Contain)): + elif isinstance(codeelement, puppetmodel.Contain): dependencies = [] for cont in codeelement.cont: d = Dependency(PuppetParser.__process_codeelement(cont, path, code)) @@ -301,28 +385,46 @@ def process_hash_value(name: str, temp_value): d.code = get_code(codeelement) dependencies.append(d) return dependencies - elif (isinstance(codeelement, (puppetmodel.Debug, puppetmodel.Fail, puppetmodel.Realize, puppetmodel.Tag))): + elif isinstance( + codeelement, + (puppetmodel.Debug, puppetmodel.Fail, puppetmodel.Realize, puppetmodel.Tag), + ): # FIXME Ignored unsupported concepts pass - elif (isinstance(codeelement, puppetmodel.Match)): + elif isinstance(codeelement, puppetmodel.Match): # FIXME Matches are not yet supported - return [list(map(lambda ce: PuppetParser.__process_codeelement(ce, path, code), codeelement.block))] - elif (isinstance(codeelement, puppetmodel.Case)): - control = PuppetParser.__process_codeelement(codeelement.control, path, code) + return [ + list( + map( + lambda ce: PuppetParser.__process_codeelement(ce, path, code), + codeelement.block, + ) + ) + ] + elif isinstance(codeelement, puppetmodel.Case): + control = PuppetParser.__process_codeelement( + codeelement.control, path, code + ) conditions = [] for match in codeelement.matches: - expressions = PuppetParser.__process_codeelement(match.expressions, path, code) + expressions = PuppetParser.__process_codeelement( + match.expressions, path, code + ) for expression in expressions: if expression != "default": - condition = ConditionalStatement(control + "==" + expression, - ConditionalStatement.ConditionType.SWITCH, False) + condition = ConditionalStatement( + control + "==" + expression, + ConditionalStatement.ConditionType.SWITCH, + False, + ) condition.line, condition.column = match.line, match.col condition.code = get_code(match) conditions.append(condition) else: - condition = ConditionalStatement("", - ConditionalStatement.ConditionType.SWITCH, True) + condition = ConditionalStatement( + "", ConditionalStatement.ConditionType.SWITCH, True + ) condition.line, condition.column = match.line, match.col condition.code = get_code(match) conditions.append(condition) @@ -330,80 +432,117 @@ def process_hash_value(name: str, temp_value): for i in range(1, len(conditions)): conditions[i - 1].else_statement = conditions[i] - return [conditions[0]] + \ - list(map(lambda ce: PuppetParser.__process_codeelement(ce, path, code), codeelement.matches)) - elif (isinstance(codeelement, puppetmodel.Selector)): - control = PuppetParser.__process_codeelement(codeelement.control, path, code) + return [conditions[0]] + list( + map( + lambda ce: PuppetParser.__process_codeelement(ce, path, code), + codeelement.matches, + ) + ) + elif isinstance(codeelement, puppetmodel.Selector): + control = PuppetParser.__process_codeelement( + codeelement.control, path, code + ) conditions = [] - + for key_element, value_element in codeelement.hash.value.items(): key = PuppetParser.__process_codeelement(key_element, path, code) value = PuppetParser.__process_codeelement(value_element, path, code) if key != "default": - condition = ConditionalStatement(control + "==" + key, - ConditionalStatement.ConditionType.SWITCH, False) + condition = ConditionalStatement( + control + "==" + key, + ConditionalStatement.ConditionType.SWITCH, + False, + ) condition.line, condition.column = key_element.line, key_element.col # HACK: the get_code function should be changed to receive a range - key_element.end_line, key_element.end_col = value_element.end_line, value_element.end_col + key_element.end_line, key_element.end_col = ( + value_element.end_line, + value_element.end_col, + ) condition.code = get_code(key_element) conditions.append(condition) else: - condition = ConditionalStatement("", - ConditionalStatement.ConditionType.SWITCH, True) + condition = ConditionalStatement( + "", ConditionalStatement.ConditionType.SWITCH, True + ) condition.line, condition.column = key_element.line, key_element.col - key_element.end_line, key_element.end_col = value_element.end_line, value_element.end_col + key_element.end_line, key_element.end_col = ( + value_element.end_line, + value_element.end_col, + ) condition.code = get_code(key_element) conditions.append(condition) - + for i in range(1, len(conditions)): conditions[i - 1].else_statement = conditions[i] return conditions[0] - elif (isinstance(codeelement, puppetmodel.Reference)): + elif isinstance(codeelement, puppetmodel.Reference): res = codeelement.type + "[" for r in codeelement.references: temp = PuppetParser.__process_codeelement(r, path, code) res += "" if temp is None else temp res += "]" return res - elif (isinstance(codeelement, puppetmodel.Function)): + elif isinstance(codeelement, puppetmodel.Function): # FIXME Functions definitions are not yet supported - return list(map(lambda ce: PuppetParser.__process_codeelement(ce, path, code), codeelement.body)) - elif (isinstance(codeelement, puppetmodel.ResourceCollector)): + return list( + map( + lambda ce: PuppetParser.__process_codeelement(ce, path, code), + codeelement.body, + ) + ) + elif isinstance(codeelement, puppetmodel.ResourceCollector): res = codeelement.resource_type + "<|" - res += PuppetParser.__process_codeelement(codeelement.search, path, code) + "|>" + res += ( + PuppetParser.__process_codeelement(codeelement.search, path, code) + + "|>" + ) return res - elif (isinstance(codeelement, puppetmodel.ResourceExpression)): + elif isinstance(codeelement, puppetmodel.ResourceExpression): resources = [] - resources.append(PuppetParser.__process_codeelement(codeelement.default, path, code)) + resources.append( + PuppetParser.__process_codeelement(codeelement.default, path, code) + ) for resource in codeelement.resources: - resources.append(PuppetParser.__process_codeelement(resource, path, code)) + resources.append( + PuppetParser.__process_codeelement(resource, path, code) + ) return resources - elif (isinstance(codeelement, puppetmodel.Chaining)): + elif isinstance(codeelement, puppetmodel.Chaining): # FIXME Chaining not yet supported res = [] op1 = PuppetParser.__process_codeelement(codeelement.op1, path, code) op2 = PuppetParser.__process_codeelement(codeelement.op2, path, code) - if isinstance(op1, list): res += op1 - else: res.append(op1) - if isinstance(op2, list): res += op2 - else: res.append(op2) + if isinstance(op1, list): + res += op1 + else: + res.append(op1) + if isinstance(op2, list): + res += op2 + else: + res.append(op2) return res - elif (isinstance(codeelement, list)): - return list(map(lambda ce: PuppetParser.__process_codeelement(ce, path, code), codeelement)) + elif isinstance(codeelement, list): + return list( + map( + lambda ce: PuppetParser.__process_codeelement(ce, path, code), + codeelement, + ) + ) elif codeelement is None: return "" else: return codeelement - + def parse_module(self, path: str) -> Module: res: Module = Module(os.path.basename(os.path.normpath(path)), path) super().parse_file_structure(res.folder, path) for root, _, files in os.walk(path, topdown=False): for name in files: - name_split = name.split('.') + name_split = name.split(".") if len(name_split) == 2 and name_split[-1] == "pp": res.add_block(self.parse_file(os.path.join(root, name), "")) @@ -412,7 +551,7 @@ def parse_module(self, path: str) -> Module: def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: unit_block: UnitBlock = UnitBlock(os.path.basename(path), UnitBlockType.script) unit_block.path = path - + try: with open(path) as f: parsed_script, comments = parse_puppet(f.read()) @@ -423,39 +562,42 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: for c in comments: comment = Comment(c.content) comment.line = c.line - comment.code = ''.join(code[c.line - 1 : c.end_line]) + comment.code = "".join(code[c.line - 1 : c.end_line]) unit_block.add_comment(comment) PuppetParser.__process_unitblock_component( PuppetParser.__process_codeelement(parsed_script, path, code), - unit_block + unit_block, ) except Exception as e: - traceback.print_exc() - throw_exception(EXCEPTIONS["PUPPET_COULD_NOT_PARSE"], path) + traceback.print_exc() + throw_exception(EXCEPTIONS["PUPPET_COULD_NOT_PARSE"], path) return unit_block def parse_folder(self, path: str) -> Project: res: Project = Project(os.path.basename(os.path.normpath(path))) if os.path.exists(f"{path}/modules") and not os.path.islink(f"{path}/modules"): - subfolders = [f.path for f in os.scandir(f"{path}/modules") - if f.is_dir() and not f.is_symlink()] + subfolders = [ + f.path + for f in os.scandir(f"{path}/modules") + if f.is_dir() and not f.is_symlink() + ] for m in subfolders: res.add_module(self.parse_module(m)) for f in os.scandir(path): - name_split = f.name.split('.') + name_split = f.name.split(".") if f.is_file() and len(name_split) == 2 and name_split[-1] == "pp": res.add_block(self.parse_file(f.path, "")) - subfolders = [f.path for f in os.scandir(f"{path}") - if f.is_dir() and not f.is_symlink()] + subfolders = [ + f.path for f in os.scandir(f"{path}") if f.is_dir() and not f.is_symlink() + ] for d in subfolders: - if os.path.basename(os.path.normpath(d)) not \ - in ["modules"]: + if os.path.basename(os.path.normpath(d)) not in ["modules"]: aux = self.parse_folder(d) res.blocks += aux.blocks res.modules += aux.modules - return res \ No newline at end of file + return res diff --git a/glitch/parsers/ripper_parser.py b/glitch/parsers/ripper_parser.py index 35ac70e2..e2c560bb 100644 --- a/glitch/parsers/ripper_parser.py +++ b/glitch/parsers/ripper_parser.py @@ -1,53 +1,61 @@ from ply.lex import lex from ply.yacc import yacc + def parser_yacc(script_ast): - tokens = ('LPAREN', 'RPAREN', 'STRING', 'ID', 'INTEGER', - 'TRUE', 'FALSE', 'COMMENT', 'PLUS') - states = ( - ('id', 'exclusive'), + tokens = ( + "LPAREN", + "RPAREN", + "STRING", + "ID", + "INTEGER", + "TRUE", + "FALSE", + "COMMENT", + "PLUS", ) + states = (("id", "exclusive"),) - t_LPAREN = r'\[' - t_RPAREN = r'\]' - t_TRUE = r'true' - t_FALSE = r'false' - t_ignore_ANY = r'[nil\,\ \n]' - t_PLUS = r'\+' + t_LPAREN = r"\[" + t_RPAREN = r"\]" + t_TRUE = r"true" + t_FALSE = r"false" + t_ignore_ANY = r"[nil\,\ \n]" + t_PLUS = r"\+" def t_INTEGER(t): - r'[0-9]+' + r"[0-9]+" t.value = int(t.value) return t def t_STRING(t): - r'\"([^\\\n]|(\\.))*?\"' + r"\"([^\\\n]|(\\.))*?\" " t.value = t.value[1:-1] return t def t_begin_id(t): - r'\:' - t.lexer.begin('id') + r"\:" + t.lexer.begin("id") def t_id_end(t): - r'[\,]' - t.lexer.begin('INITIAL') + r"[\,]" + t.lexer.begin("INITIAL") def t_id_RPAREN(t): - r'\]' - t.lexer.begin('INITIAL') + r"\]" + t.lexer.begin("INITIAL") return t def t_id_COMMENT(t): - r'@comment' + r"@comment" return t def t_id_ID(t): - r'[^,\]]+' + r"[^,\]]+" return t def t_ANY_error(t): - print(f'Illegal character {t.value[0]!r}.') + print(f"Illegal character {t.value[0]!r}.") t.lexer.skip(1) lexer = lex() @@ -55,71 +63,71 @@ def t_ANY_error(t): lexer.input(script_ast) def p_program(p): - r'program : comments list' + r"program : comments list" p[0] = (p[1], p[2]) def p_comments(p): - r'comments : comments comment' + r"comments : comments comment" p[0] = [p[2]] + p[1] def p_comments_empty(p): - r'comments : empty' + r"comments : empty" p[0] = [] def p_comment(p): - r'comment : LPAREN COMMENT STRING LPAREN INTEGER INTEGER RPAREN RPAREN' + r"comment : LPAREN COMMENT STRING LPAREN INTEGER INTEGER RPAREN RPAREN" p[0] = (p[3], p[5]) def p_list(p): - r'list : LPAREN args RPAREN' + r"list : LPAREN args RPAREN" p[0] = p[2] def p_args_value(p): - r'args : value args' + r"args : value args" p[0] = [p[1]] + p[2] def p_args_list(p): - r'args : list args' + r"args : list args" p[0] = [p[1]] + p[2] def p_args_empty(p): - r'args : empty' + r"args : empty" p[0] = [] def p_empty(p): - r'empty : ' + r"empty :" def p_value_string(p): - r'value : string' + r"value : string" p[0] = p[1] - + def p_multi_string(p): - r'string : STRING PLUS string' + r"string : STRING PLUS string" p[0] = p[1] + p[3] def p_string(p): - r'string : STRING' + r"string : STRING" p[0] = p[1] def p_value_integer(p): - r'value : INTEGER' + r"value : INTEGER" p[0] = p[1] def p_value_false(p): - r'value : FALSE' + r"value : FALSE" p[0] = False - + def p_value_true(p): - r'value : TRUE' + r"value : TRUE" p[0] = True def p_value_id(p): - r'value : ID' - p[0] = ("id", p[1]) #FIXME + r"value : ID" + p[0] = ("id", p[1]) # FIXME def p_error(p): - print(f'Syntax error at {p.value!r}') + print(f"Syntax error at {p.value!r}") # Build the parser parser = yacc() - return parser.parse(script_ast) \ No newline at end of file + return parser.parse(script_ast) diff --git a/glitch/parsers/terraform.py b/glitch/parsers/terraform.py index f9437b07..02ae3de5 100644 --- a/glitch/parsers/terraform.py +++ b/glitch/parsers/terraform.py @@ -10,21 +10,23 @@ class TerraformParser(p.Parser): @staticmethod def __get_element_code(start_line, end_line, code): - lines = code[start_line-1:end_line] - res = '' + lines = code[start_line - 1 : end_line] + res = "" for line in lines: res += line return res - def parse_keyvalues(self, unit_block: UnitBlock, keyvalues, code, type: str): def create_keyvalue(start_line, end_line, name: str, value: str): - has_variable = ("${" in f"{value}") and ("}" in f"{value}") if value != None else False - pattern = r'^[+-]?\d+(\.\d+)?$' - if (has_variable and re.match(pattern, re.sub(r'^\${(.*)}$', r'\1', value))): - value = re.sub(r'^\${(.*)}$', r'\1', value) + has_variable = ( + ("${" in f"{value}") and ("}" in f"{value}") if value != None else False + ) + pattern = r"^[+-]?\d+(\.\d+)?$" + if has_variable and re.match(pattern, re.sub(r"^\${(.*)}$", r"\1", value)): + value = re.sub(r"^\${(.*)}$", r"\1", value) has_variable = False - if value == "null": value = "" + if value == "null": + value = "" if isinstance(value, int): value = str(value) @@ -34,9 +36,11 @@ def create_keyvalue(start_line, end_line, name: str, value: str): elif type == "variable": keyvalue = Variable(str(name), value, has_variable) keyvalue.line = start_line - keyvalue.code = TerraformParser.__get_element_code(start_line, end_line, code) + keyvalue.code = TerraformParser.__get_element_code( + start_line, end_line, code + ) return keyvalue - + def process_list(name, value, start_line, end_line): for i, v in enumerate(value): if isinstance(v, dict): @@ -51,44 +55,70 @@ def process_list(name, value, start_line, end_line): k_values = [] for name, keyvalue in keyvalues.items(): - if name == "__start_line__" or name == "__end_line__": + if name == "__start_line__" or name == "__end_line__": continue - if isinstance(keyvalue, dict): # Note: local values (variables) can only enter here + if isinstance( + keyvalue, dict + ): # Note: local values (variables) can only enter here value = keyvalue["value"] - if isinstance(value, dict): # (ex: labels = {}) - k = create_keyvalue(keyvalue["__start_line__"], keyvalue["__end_line__"], name, None) + if isinstance(value, dict): # (ex: labels = {}) + k = create_keyvalue( + keyvalue["__start_line__"], keyvalue["__end_line__"], name, None + ) k.keyvalues = self.parse_keyvalues(unit_block, value, code, type) k_values.append(k) - elif isinstance(value, list): # (ex: x = [1,2,3]) - process_list(name, value, keyvalue["__start_line__"], keyvalue["__end_line__"]) - else: # (ex: x = 'test') - if value == None: # (ex: x = null) + elif isinstance(value, list): # (ex: x = [1,2,3]) + process_list( + name, + value, + keyvalue["__start_line__"], + keyvalue["__end_line__"], + ) + else: # (ex: x = 'test') + if value == None: # (ex: x = null) value = "null" - k = create_keyvalue(keyvalue["__start_line__"], keyvalue["__end_line__"], name, value) - k_values.append(k) + k = create_keyvalue( + keyvalue["__start_line__"], + keyvalue["__end_line__"], + name, + value, + ) + k_values.append(k) elif isinstance(keyvalue, list) and type == "attribute": - # block (ex: access {} or dynamic setting {}; blocks of attributes; not allowed inside local values (variables)) + # block (ex: access {} or dynamic setting {}; blocks of attributes; not allowed inside local values (variables)) try: for block_attributes in keyvalue: - k = create_keyvalue(block_attributes["__start_line__"], - block_attributes["__end_line__"], name, None) - k.keyvalues = self.parse_keyvalues(unit_block, block_attributes, code, type) + k = create_keyvalue( + block_attributes["__start_line__"], + block_attributes["__end_line__"], + name, + None, + ) + k.keyvalues = self.parse_keyvalues( + unit_block, block_attributes, code, type + ) k_values.append(k) except KeyError: for block in keyvalue: for block_name, block_attributes in block.items(): - k = create_keyvalue(block_attributes["__start_line__"], - block_attributes["__end_line__"], f"{name}.{block_name}", None) - k.keyvalues = self.parse_keyvalues(unit_block, block_attributes, code, type) + k = create_keyvalue( + block_attributes["__start_line__"], + block_attributes["__end_line__"], + f"{name}.{block_name}", + None, + ) + k.keyvalues = self.parse_keyvalues( + unit_block, block_attributes, code, type + ) k_values.append(k) - - - return k_values + return k_values def parse_atomic_unit(self, type: str, unit_block: UnitBlock, dict, code): - def create_atomic_unit(start_line, end_line, type: str, name: str, code) -> AtomicUnit: + def create_atomic_unit( + start_line, end_line, type: str, name: str, code + ) -> AtomicUnit: au = AtomicUnit(name, type) au.line = start_line au.code = TerraformParser.__get_element_code(start_line, end_line, code) @@ -97,15 +127,30 @@ def create_atomic_unit(start_line, end_line, type: str, name: str, code) -> Atom def parse_resource(): for resource_type, resource in dict.items(): for name, attributes in resource.items(): - au = create_atomic_unit(attributes['__start_line__'], - attributes['__end_line__'], f"{type}.{resource_type}", name, code) - au.attributes = self.parse_keyvalues(unit_block, attributes, code, "attribute") + au = create_atomic_unit( + attributes["__start_line__"], + attributes["__end_line__"], + f"{type}.{resource_type}", + name, + code, + ) + au.attributes = self.parse_keyvalues( + unit_block, attributes, code, "attribute" + ) unit_block.add_atomic_unit(au) def parse_simple_unit(): for name, attributes in dict.items(): - au = create_atomic_unit(attributes['__start_line__'], attributes['__end_line__'], type, name, code) - au.attributes = self.parse_keyvalues(unit_block, attributes, code, "attribute") + au = create_atomic_unit( + attributes["__start_line__"], + attributes["__end_line__"], + type, + name, + code, + ) + au.attributes = self.parse_keyvalues( + unit_block, attributes, code, "attribute" + ) unit_block.add_atomic_unit(au) if type in ["resource", "data"]: @@ -113,17 +158,22 @@ def parse_simple_unit(): elif type in ["variable", "module", "output"]: parse_simple_unit() - def parse_comments(self, unit_block: UnitBlock, comments, code): def create_comment(value, start_line, end_line, code): c = Comment(value) c.line = start_line c.code = TerraformParser.__get_element_code(start_line, end_line, code) return c - - for comment in comments: - unit_block.add_comment(create_comment(comment["value"], comment["__start_line__"], comment["__end_line__"], code)) + for comment in comments: + unit_block.add_comment( + create_comment( + comment["value"], + comment["__start_line__"], + comment["__end_line__"], + code, + ) + ) def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: unit_block = UnitBlock(path, type) @@ -141,7 +191,9 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: self.parse_comments(unit_block, value, code) elif key == "locals": for local in value: - unit_block.variables += self.parse_keyvalues(unit_block, local, code, "variable") + unit_block.variables += self.parse_keyvalues( + unit_block, local, code, "variable" + ) elif key in ["provider", "terraform"]: continue else: @@ -150,29 +202,29 @@ def parse_file(self, path: str, type: UnitBlockType) -> UnitBlock: throw_exception(EXCEPTIONS["TERRAFORM_COULD_NOT_PARSE"], path) return unit_block - def parse_module(self, path: str) -> Module: res: Module = Module(os.path.basename(os.path.normpath(path)), path) super().parse_file_structure(res.folder, path) - files = [f.path for f in os.scandir(f"{path}") - if f.is_file() and not f.is_symlink()] + files = [ + f.path for f in os.scandir(f"{path}") if f.is_file() and not f.is_symlink() + ] for f in files: unit_block = self.parse_file(f, "unknown") res.add_block(unit_block) - - return res + return res def parse_folder(self, path: str) -> Project: res: Project = Project(os.path.basename(os.path.normpath(path))) res.add_module(self.parse_module(path)) - subfolders = [f.path for f in os.scandir(f"{path}") - if f.is_dir() and not f.is_symlink()] + subfolders = [ + f.path for f in os.scandir(f"{path}") if f.is_dir() and not f.is_symlink() + ] for d in subfolders: aux = self.parse_folder(d) res.blocks += aux.blocks res.modules += aux.modules - return res \ No newline at end of file + return res diff --git a/glitch/repair/interactive/compiler/compiler.py b/glitch/repair/interactive/compiler/compiler.py index 1facc665..e59b5489 100644 --- a/glitch/repair/interactive/compiler/compiler.py +++ b/glitch/repair/interactive/compiler/compiler.py @@ -67,15 +67,13 @@ def create_label_var_pair( label = labeled_script.get_label(attr) else: # Creates sketched attribute - if attr_name == "state": # HACK + if attr_name == "state": # HACK attr = Attribute( - attr_name, - DefaultValue.DEFAULT_STATE.const.value, - False + attr_name, DefaultValue.DEFAULT_STATE.const.value, False ) else: attr = Attribute(attr_name, PEUndef(), False) - + attr.line, attr.column = ( DeltaPCompiler._sketched, DeltaPCompiler._sketched, @@ -173,10 +171,10 @@ def __handle_file( @staticmethod def __handle_atomic_unit( - statement: PStatement, - atomic_unit: AtomicUnit, - tech: Tech, - labeled_script: LabeledUnitBlock + statement: PStatement, + atomic_unit: AtomicUnit, + tech: Tech, + labeled_script: LabeledUnitBlock, ) -> PStatement: attributes: DeltaPCompiler.__Attributes = DeltaPCompiler.__Attributes( atomic_unit.type, tech @@ -192,9 +190,7 @@ def __handle_atomic_unit( @staticmethod def __handle_conditional( - conditional: ConditionalStatement, - tech: Tech, - labeled_script: LabeledUnitBlock + conditional: ConditionalStatement, tech: Tech, labeled_script: LabeledUnitBlock ) -> PStatement: body = PSkip() for stat in conditional.statements: @@ -205,25 +201,23 @@ def __handle_conditional( elif isinstance(stat, ConditionalStatement): body = PSeq( body, - DeltaPCompiler.__handle_conditional( - stat, tech, labeled_script - ) + DeltaPCompiler.__handle_conditional(stat, tech, labeled_script), ) - - else_statement = PSkip() + + else_statement = PSkip() if conditional.else_statement is not None: else_statement = DeltaPCompiler.__handle_conditional( conditional.else_statement, tech, labeled_script ) - + DeltaPCompiler._condition += 1 return PIf( # FIXME: This creates a placeholder since we will branch every time # There are cases that we can infer the value of the condition # The creation of these variables should be done in the solver - PEVar(f"dejavu-condition-{DeltaPCompiler._condition}"), - body, - else_statement + PEVar(f"dejavu-condition-{DeltaPCompiler._condition}"), + body, + else_statement, ) @staticmethod @@ -237,9 +231,10 @@ def compile(labeled_script: LabeledUnitBlock, tech: Tech) -> PStatement: for stat in script.statements: if isinstance(stat, ConditionalStatement): - statement = PSeq(statement, DeltaPCompiler.__handle_conditional( - stat, tech, labeled_script - )) + statement = PSeq( + statement, + DeltaPCompiler.__handle_conditional(stat, tech, labeled_script), + ) for atomic_unit in script.atomic_units: statement = DeltaPCompiler.__handle_atomic_unit( diff --git a/glitch/repair/interactive/compiler/labeler.py b/glitch/repair/interactive/compiler/labeler.py index f22ee94d..6cfbd68a 100644 --- a/glitch/repair/interactive/compiler/labeler.py +++ b/glitch/repair/interactive/compiler/labeler.py @@ -27,12 +27,12 @@ def add_label( self, name: str, codeelement: CodeElement, sketched: bool = False ) -> int: """Adds a label to the code element with the given name. - + Args: name (str): The name of the code element. codeelement (CodeElement): The code element to be labeled. sketched (bool): Whether the code element is sketched. - + Returns: int: The label of the code element. """ @@ -45,12 +45,12 @@ def add_label( self.__label_to_var[self.__label] = f"sketched-{var}" self.__label += 1 return self.__label - 1 - + def add_sketch_location( self, sketch_location: CodeElement, codeelement: CodeElement ): """Defines where a sketched code element is defined in the script. - + Args: sketch_location (CodeElement): The code element where the sketched code element is defined. codeelement (CodeElement): The sketched code element. @@ -78,12 +78,12 @@ def get_codeelement(self, label: int) -> CodeElement: CodeElement: The code element with the given label. """ return self.__label_to_codeelement[label] - + def remove_label(self, codeelement: CodeElement): """Removes the label of the code element. - + Args: - codeelement (CodeElement): The code element. + codeelement (CodeElement): The code element. """ label = self.get_label(codeelement) del self.__codeelement_to_label[codeelement] @@ -92,18 +92,18 @@ def remove_label(self, codeelement: CodeElement): def get_var(self, label: int) -> str: """Returns the variable with the given label. - + Args: label (int): The label of the variable. """ return self.__label_to_var[label] - + def get_sketch_location(self, codeelement: CodeElement) -> CodeElement: """Returns the location where the sketched code element is defined. - + Args: codeelement (CodeElement): The sketched code element. - + Returns: CodeElement: The location where the sketched code element is defined. """ @@ -116,7 +116,7 @@ def label_attribute( labeled: LabeledUnitBlock, atomic_unit: AtomicUnit, attribute: Attribute ): """Labels an attribute. - + Args: labeled (LabeledUnitBlock): The labeled script. atomic_unit (AtomicUnit): The attribute's atomic unit. @@ -127,11 +127,9 @@ def label_attribute( labeled.add_label(name, attribute) @staticmethod - def label_atomic_unit( - labeled: LabeledUnitBlock, atomic_unit: AtomicUnit - ): + def label_atomic_unit(labeled: LabeledUnitBlock, atomic_unit: AtomicUnit): """Labels an atomic unit. - + Args: labeled (LabeledUnitBlock): The labeled script. atomic_unit (AtomicUnit): The atomic unit. @@ -142,7 +140,7 @@ def label_atomic_unit( @staticmethod def label_variable(labeled: LabeledUnitBlock, variable: Variable): """Labels a variable. - + Args: labeled (LabeledUnitBlock): The labeled script. variable (Variable): The variable. @@ -150,11 +148,9 @@ def label_variable(labeled: LabeledUnitBlock, variable: Variable): labeled.add_label(variable.name, variable) @staticmethod - def label_conditional( - labeled: LabeledUnitBlock, conditional: ConditionalStatement - ): + def label_conditional(labeled: LabeledUnitBlock, conditional: ConditionalStatement): """Labels a conditional statement. - + Args: labeled (LabeledUnitBlock): The labeled script. conditional (ConditionalStatement): The conditional statement. @@ -168,18 +164,16 @@ def label_conditional( GLITCHLabeler.label_variable(labeled, statement) if conditional.else_statement is not None: - GLITCHLabeler.label_conditional( - labeled, conditional.else_statement - ) + GLITCHLabeler.label_conditional(labeled, conditional.else_statement) @staticmethod def label(script: UnitBlock, tech: Tech) -> LabeledUnitBlock: """Labels a script. - + Args: script (UnitBlock): The script being labeled. tech (Tech): The tech being considered. - + Returns: LabeledUnitBlock: The labeled script. """ diff --git a/glitch/repair/interactive/compiler/names_database.py b/glitch/repair/interactive/compiler/names_database.py index dfcfe31a..db66d3d1 100644 --- a/glitch/repair/interactive/compiler/names_database.py +++ b/glitch/repair/interactive/compiler/names_database.py @@ -20,16 +20,16 @@ def get_au_type(type: str, tech: Tech) -> str: case "ansible.builtin.file", Tech.ansible: return "file" return None - + @staticmethod def reverse_attr_name(name: str, au_type: str, tech: Tech) -> Optional[str]: """Returns the technology-specific name of the attribute with the given name, atomic unit type and tech. - + Args: name (str): The name of the attribute. au_type (str): The type of the atomic unit. tech (Tech): The tech being considered. - + Returns: str: The technology-specific name of the attribute. """ @@ -47,9 +47,11 @@ def reverse_attr_name(name: str, au_type: str, tech: Tech) -> Optional[str]: case "state", "file", Tech.puppet: return "ensure" return None - + @staticmethod - def reverse_attr_value(value: str, attr_name: str, au_type: str, tech: Tech) -> Optional[str]: + def reverse_attr_value( + value: str, attr_name: str, au_type: str, tech: Tech + ) -> Optional[str]: """Returns the technology-specific value of the attribute with the given value, attribute name, atomic unit type and tech. Args: @@ -95,9 +97,11 @@ def get_attr_name(name: str, au_type: str, tech: Tech) -> Optional[str]: return "state" return None - + @staticmethod - def get_attr_value(value: str, name: str, au_type: str, tech: Tech) -> Optional[str]: + def get_attr_value( + value: str, name: str, au_type: str, tech: Tech + ) -> Optional[str]: """Returns the generic value of the attribute with the given value, name, atomic unit type and tech. @@ -115,5 +119,5 @@ def get_attr_value(value: str, name: str, au_type: str, tech: Tech) -> Optional[ return "present" case "touch", "state", "file", Tech.ansible: return "present" - + return value diff --git a/glitch/repair/interactive/delta_p.py b/glitch/repair/interactive/delta_p.py index 8b31567d..ce599b8f 100644 --- a/glitch/repair/interactive/delta_p.py +++ b/glitch/repair/interactive/delta_p.py @@ -225,7 +225,7 @@ def to_filesystems( fss = [fss.copy()] elif fss == []: fss = [FileSystemState()] - + if vars is None: vars = {} @@ -282,7 +282,7 @@ def to_filesystems( if self.cons == PSkip() and self.alt == PSkip(): res_fss.append(fs) continue - + res_fss.append(fs) return res_fss diff --git a/glitch/repair/interactive/solver.py b/glitch/repair/interactive/solver.py index ed4e0d82..f7f138b0 100644 --- a/glitch/repair/interactive/solver.py +++ b/glitch/repair/interactive/solver.py @@ -17,7 +17,7 @@ Sum, ModelRef, Z3PPObject, - Context + Context, ) from glitch.repair.interactive.filesystem import FileSystemState @@ -42,11 +42,11 @@ class __Funs: owner_fun: Fun def __init__( - self, - statement: PStatement, - filesystem: FileSystemState, + self, + statement: PStatement, + filesystem: FileSystemState, timeout: int = 180, - ctx: Optional[Context] = None + ctx: Optional[Context] = None, ): # FIXME: the filesystem in here should be generated from # checking the affected paths in statement @@ -310,10 +310,11 @@ def solve(self) -> Optional[List[ModelRef]]: def __find_atomic_unit( labeled_script: LabeledUnitBlock, attribute: Attribute ) -> AtomicUnit: - def aux_find_atomic_unit( - code_element: CodeElement - ) -> AtomicUnit: - if isinstance(code_element, AtomicUnit) and attribute in code_element.attributes: + def aux_find_atomic_unit(code_element: CodeElement) -> AtomicUnit: + if ( + isinstance(code_element, AtomicUnit) + and attribute in code_element.attributes + ): return code_element elif isinstance(code_element, Block): for statement in code_element.statements: @@ -322,8 +323,9 @@ def aux_find_atomic_unit( return result return None - code_elements = (labeled_script.script.statements - + labeled_script.script.atomic_units) + code_elements = ( + labeled_script.script.statements + labeled_script.script.atomic_units + ) for code_element in code_elements: result = aux_find_atomic_unit(code_element) if result is not None: @@ -361,9 +363,7 @@ def apply_patch(self, model_ref: ModelRef, labeled_script: LabeledUnitBlock): atomic_unit.attributes.append(codeelement) # Remove sketch label and add regular label labeled_script.remove_label(codeelement) - GLITCHLabeler.label_attribute( - labeled_script, atomic_unit, codeelement - ) + GLITCHLabeler.label_attribute(labeled_script, atomic_unit, codeelement) else: atomic_unit = PatchSolver.__find_atomic_unit( labeled_script, codeelement diff --git a/glitch/repair/interactive/tracer/tracer.py b/glitch/repair/interactive/tracer/tracer.py index fa19d340..35ddf4d9 100644 --- a/glitch/repair/interactive/tracer/tracer.py +++ b/glitch/repair/interactive/tracer/tracer.py @@ -50,4 +50,3 @@ def run(self) -> None: self.syscalls.append(syscall) return self.syscalls - diff --git a/glitch/repair/interactive/values.py b/glitch/repair/interactive/values.py index a81f9239..83d4d93a 100644 --- a/glitch/repair/interactive/values.py +++ b/glitch/repair/interactive/values.py @@ -2,6 +2,7 @@ UNDEF = "glitch-undef" + class DefaultValue: DEFAULT_MODE = PEConst(PStr("644")) DEFAULT_OWNER = PEConst(PStr("root")) diff --git a/glitch/repr/inter.py b/glitch/repr/inter.py index 9ef3e359..13cfb5f7 100644 --- a/glitch/repr/inter.py +++ b/glitch/repr/inter.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from enum import Enum + class CodeElement(ABC): def __init__(self) -> None: self.line: int = -1 @@ -9,7 +10,7 @@ def __init__(self) -> None: def __hash__(self) -> int: return hash(self.line) * hash(self.column) - + def __eq__(self, o: object) -> bool: if not isinstance(o, CodeElement): return False @@ -22,6 +23,7 @@ def __str__(self) -> str: def print(self, tab): pass + class Block(CodeElement): def __init__(self) -> None: super().__init__() @@ -30,6 +32,7 @@ def __init__(self) -> None: def add_statement(self, statement): self.statements.append(statement) + class ConditionalStatement(Block): class ConditionType(Enum): IF = 1 @@ -43,14 +46,23 @@ def __init__(self, condition: str, type, is_default=False) -> None: self.type = type def __repr__(self) -> str: - return self.code.strip().split('\n')[0] + return self.code.strip().split("\n")[0] def print(self, tab) -> str: - res = (tab * "\t") + str(self.type) + " " + self.condition + \ - ("" if not self.is_default else "default") + ' (on line ' + str(self.line) + ')' + "\n" + res = ( + (tab * "\t") + + str(self.type) + + " " + + self.condition + + ("" if not self.is_default else "default") + + " (on line " + + str(self.line) + + ")" + + "\n" + ) res += (tab * "\t") + "\telse:\n" - if (self.else_statement is not None): + if self.else_statement is not None: res += self.else_statement.print(tab + 2) + "\n" res += (tab * "\t") + "\tblock:\n" @@ -60,6 +72,7 @@ def print(self, tab) -> str: return res + class Comment(CodeElement): def __init__(self, content: str) -> None: super().__init__() @@ -69,7 +82,8 @@ def __repr__(self) -> str: return self.content def print(self, tab) -> str: - return (tab * "\t") + self.content + ' (on line ' + str(self.line) + ')' + return (tab * "\t") + self.content + " (on line " + str(self.line) + ")" + class KeyValue(CodeElement): def __init__(self, name: str, value: str, has_variable: bool): @@ -79,27 +93,51 @@ def __init__(self, name: str, value: str, has_variable: bool): self.keyvalues: list = [] def __repr__(self) -> str: - value = repr(self.value).split('\n')[0] + value = repr(self.value).split("\n")[0] if value == "None": return f"{self.name}:{value}:{self.keyvalues}" else: return f"{self.name}:{value}" + class Variable(KeyValue): def __init__(self, name: str, value: str, has_variable: bool) -> None: super().__init__(name, value, has_variable) def print(self, tab) -> str: if isinstance(self.value, str): - return (tab * "\t") + self.name + "->" + self.value + \ - " (on line " + str(self.line) + f" {self.has_variable})" + return ( + (tab * "\t") + + self.name + + "->" + + self.value + + " (on line " + + str(self.line) + + f" {self.has_variable})" + ) elif isinstance(self.value, type(None)): - return (tab * "\t") + self.name + "->" + "None" + \ - " variables:" + f" {self.keyvalues}" + \ - " (on line " + str(self.line) + f" {self.has_variable})" + return ( + (tab * "\t") + + self.name + + "->" + + "None" + + " variables:" + + f" {self.keyvalues}" + + " (on line " + + str(self.line) + + f" {self.has_variable})" + ) else: - return (tab * "\t") + self.name + "->" + repr(self.value) + \ - " (on line " + str(self.line) + f" {self.has_variable})" + return ( + (tab * "\t") + + self.name + + "->" + + repr(self.value) + + " (on line " + + str(self.line) + + f" {self.has_variable})" + ) + class Attribute(KeyValue): def __init__(self, name: str, value: str, has_variable: bool) -> None: @@ -107,15 +145,38 @@ def __init__(self, name: str, value: str, has_variable: bool) -> None: def print(self, tab) -> str: if isinstance(self.value, str): - return (tab * "\t") + self.name + "->" + self.value + \ - " (on line " + str(self.line) + f" {self.has_variable})" + return ( + (tab * "\t") + + self.name + + "->" + + self.value + + " (on line " + + str(self.line) + + f" {self.has_variable})" + ) elif isinstance(self.value, type(None)): - return (tab * "\t") + self.name + "->" + "None" + \ - " attributes:" + f" {self.keyvalues}" + \ - " (on line " + str(self.line) + f" {self.has_variable})" + return ( + (tab * "\t") + + self.name + + "->" + + "None" + + " attributes:" + + f" {self.keyvalues}" + + " (on line " + + str(self.line) + + f" {self.has_variable})" + ) else: - return (tab * "\t") + self.name + "->" + repr(self.value) + \ - " (on line " + str(self.line) + f" {self.has_variable})" + return ( + (tab * "\t") + + self.name + + "->" + + repr(self.value) + + " (on line " + + str(self.line) + + f" {self.has_variable})" + ) + class AtomicUnit(Block): def __init__(self, name: str, type: str) -> None: @@ -131,8 +192,15 @@ def __repr__(self) -> str: return f"{self.name} {self.type}" def print(self, tab) -> str: - res = ((tab * "\t") + self.type + ' ' + self.name + " (on line " - + str(self.line) + ")\n") + res = ( + (tab * "\t") + + self.type + + " " + + self.name + + " (on line " + + str(self.line) + + ")\n" + ) for attribute in self.attributes: res += attribute.print(tab + 1) + "\n" @@ -144,6 +212,7 @@ def print(self, tab) -> str: return res + class Dependency(CodeElement): def __init__(self, name: str) -> None: super().__init__() @@ -155,6 +224,7 @@ def __repr__(self) -> str: def print(self, tab) -> str: return (tab * "\t") + self.name + " (on line " + str(self.line) + ")" + class UnitBlockType(str, Enum): script = "script" tasks = "tasks" @@ -162,6 +232,7 @@ class UnitBlockType(str, Enum): block = "block" unknown = "unknown" + class UnitBlock(Block): def __init__(self, name: str, type: UnitBlockType) -> None: super().__init__() @@ -169,7 +240,7 @@ def __init__(self, name: str, type: UnitBlockType) -> None: self.comments: list[Comment] = [] self.variables: list[Variable] = [] self.atomic_units: list[AtomicUnit] = [] - self.unit_blocks: list['UnitBlock'] = [] + self.unit_blocks: list["UnitBlock"] = [] self.attributes: list[Attribute] = [] self.name: str = name self.path: str = "" @@ -190,7 +261,7 @@ def add_variable(self, v: Variable) -> None: def add_atomic_unit(self, a: AtomicUnit) -> None: self.atomic_units.append(a) - def add_unit_block(self, u: 'UnitBlock') -> None: + def add_unit_block(self, u: "UnitBlock") -> None: self.unit_blocks.append(u) def add_attribute(self, a: Attribute) -> None: @@ -198,7 +269,7 @@ def add_attribute(self, a: Attribute) -> None: def print(self, tab) -> str: res = (tab * "\t") + self.name + "\n" - + res += (tab * "\t") + "\tdependencies:\n" for dependency in self.dependencies: res += dependency.print(tab + 2) + "\n" @@ -229,6 +300,7 @@ def print(self, tab) -> str: return res + class File: def __init__(self, name) -> None: self.name: str = name @@ -236,12 +308,13 @@ def __init__(self, name) -> None: def print(self, tab) -> str: return (tab * "\t") + self.name + class Folder: def __init__(self, name) -> None: self.content: list = [] self.name: str = name - def add_folder(self, folder: 'Folder') -> None: + def add_folder(self, folder: "Folder") -> None: self.content.append(folder) def add_file(self, file: File) -> None: @@ -256,6 +329,7 @@ def print(self, tab) -> str: return res + class Module: def __init__(self, name, path) -> None: self.name: str = name @@ -281,6 +355,7 @@ def print(self, tab) -> str: return res + class Project: def __init__(self, name) -> None: self.name: str = name @@ -292,7 +367,7 @@ def __repr__(self) -> str: def add_module(self, m: Module): self.modules.append(m) - + def add_block(self, u: UnitBlock): self.blocks.append(u) @@ -307,4 +382,4 @@ def print(self, tab) -> str: for block in self.blocks: res += block.print(tab + 2) - return res \ No newline at end of file + return res diff --git a/glitch/stats/print.py b/glitch/stats/print.py index e3d03ef7..a64457b2 100644 --- a/glitch/stats/print.py +++ b/glitch/stats/print.py @@ -3,10 +3,11 @@ from glitch.analysis.rules import Error from prettytable import PrettyTable + def print_stats(errors, smells, file_stats, format): total_files = len(file_stats.files) occurrences = {} - files_with_the_smell = {'Combined': set()} + files_with_the_smell = {"Combined": set()} for smell in smells: occurrences[smell] = 0 @@ -15,35 +16,46 @@ def print_stats(errors, smells, file_stats, format): for error in errors: occurrences[error.code] += 1 files_with_the_smell[error.code].add(error.path) - files_with_the_smell['Combined'].add(error.path) - + files_with_the_smell["Combined"].add(error.path) + stats_info = [] total_occur = 0 total_smell_density = 0 for code, n in occurrences.items(): total_occur += n total_smell_density += round(n / (file_stats.loc / 1000), 2) - stats_info.append([Error.ALL_ERRORS[code], n, - round(n / (file_stats.loc / 1000), 2), - round((len(files_with_the_smell[code]) / total_files) * 100, 2)]) - stats_info.append([ - "Combined", - total_occur, - total_smell_density, - round((len(files_with_the_smell['Combined']) / total_files) * 100, 2) - ]) + stats_info.append( + [ + Error.ALL_ERRORS[code], + n, + round(n / (file_stats.loc / 1000), 2), + round((len(files_with_the_smell[code]) / total_files) * 100, 2), + ] + ) + stats_info.append( + [ + "Combined", + total_occur, + total_smell_density, + round((len(files_with_the_smell["Combined"]) / total_files) * 100, 2), + ] + ) - if (format == "prettytable"): + if format == "prettytable": table = PrettyTable() - table.field_names = ["Smell", "Occurrences", - "Smell density (Smell/KLoC)", "Proportion of scripts (%)"] - table.align["Smell"] = 'r' - table.align["Occurrences"] = 'l' - table.align["Smell density (Smell/KLoC)"] = 'l' - table.align["Proportion of scripts (%)"] = 'l' + table.field_names = [ + "Smell", + "Occurrences", + "Smell density (Smell/KLoC)", + "Proportion of scripts (%)", + ] + table.align["Smell"] = "r" + table.align["Occurrences"] = "l" + table.align["Smell density (Smell/KLoC)"] = "l" + table.align["Proportion of scripts (%)"] = "l" smells_info = stats_info[:-1] for smell in smells_info: - smell[0] = smell[0].split(' - ')[0] + smell[0] = smell[0].split(" - ")[0] smells_info = sorted(smells_info, key=lambda x: x[0]) biggest_value = [len(name) for name in table.field_names] @@ -53,7 +65,7 @@ def print_stats(errors, smells, file_stats, format): biggest_value[i] = len(str(s)) table.add_row(stats) - + div_row = [i * "-" for i in biggest_value] table.add_row(div_row) table.add_row(stats_info[-1]) @@ -63,21 +75,36 @@ def print_stats(errors, smells, file_stats, format): attributes.field_names = ["Total IaC files", "Lines of Code"] attributes.add_row([total_files, file_stats.loc]) print(attributes) - elif (format == "latex"): + elif format == "latex": smells_info = stats_info[:-1] smells_info = sorted(smells_info, key=lambda x: x[0]) for smell in smells_info: - smell[0] = smell[0].split(' - ')[0] + smell[0] = smell[0].split(" - ")[0] smells_info.append(stats_info[-1]) - table = pd.DataFrame(smells_info, columns = ["\\textbf{Smell}", "\\textbf{Occurrences}", - "\\textbf{Smell density (Smell/KLoC)}", "\\textbf{Proportion of scripts (\%)}"]) - latex = table.style.hide(axis='index').format(escape=None, - precision=2, thousands=',').to_latex() - combined = latex[:latex.rfind('\\\\')].rfind('\\\\') - latex = latex[:combined] + "\\\\\n\midrule\n" + latex[combined + 3:] + table = pd.DataFrame( + smells_info, + columns=[ + "\\textbf{Smell}", + "\\textbf{Occurrences}", + "\\textbf{Smell density (Smell/KLoC)}", + "\\textbf{Proportion of scripts (\%)}", + ], + ) + latex = ( + table.style.hide(axis="index") + .format(escape=None, precision=2, thousands=",") + .to_latex() + ) + combined = latex[: latex.rfind("\\\\")].rfind("\\\\") + latex = latex[:combined] + "\\\\\n\midrule\n" + latex[combined + 3 :] print(latex) - attributes = pd.DataFrame([[total_files, file_stats.loc]], columns= - ["\\textbf{Total IaC files}", "\\textbf{Lines of Code}"]) - print(attributes.style.hide(axis='index').format(escape=None, - precision=2, thousands=',').to_latex()) \ No newline at end of file + attributes = pd.DataFrame( + [[total_files, file_stats.loc]], + columns=["\\textbf{Total IaC files}", "\\textbf{Lines of Code}"], + ) + print( + attributes.style.hide(axis="index") + .format(escape=None, precision=2, thousands=",") + .to_latex() + ) diff --git a/glitch/stats/stats.py b/glitch/stats/stats.py index 4dbfbfd3..8acd56af 100644 --- a/glitch/stats/stats.py +++ b/glitch/stats/stats.py @@ -3,6 +3,7 @@ from glitch.repr.inter import * + class Stats(ABC): def compute(self, c): if isinstance(c, Project): @@ -64,6 +65,7 @@ def compute_condition(self, c: ConditionalStatement): def compute_comment(self, c: Comment): pass + class FileStats(Stats): def __init__(self) -> None: super().__init__() @@ -111,4 +113,4 @@ def compute_condition(self, c: ConditionalStatement): pass def compute_comment(self, c: Comment): - pass \ No newline at end of file + pass diff --git a/glitch/tests/design/ansible/test_design.py b/glitch/tests/design/ansible/test_design.py index df599559..22de353f 100644 --- a/glitch/tests/design/ansible/test_design.py +++ b/glitch/tests/design/ansible/test_design.py @@ -4,25 +4,33 @@ from glitch.parsers.ansible import AnsibleParser from glitch.tech import Tech + class TestDesign(unittest.TestCase): def __help_test(self, path, type, n_errors, codes, lines): parser = AnsibleParser() inter = parser.parse(path, type, False) analysis = DesignVisitor(Tech.ansible) analysis.config("tests/design/ansible/design_ansible.ini") - errors = list(filter(lambda e: e.code.startswith('design_') - or e.code.startswith('implementation_'), set(analysis.check(inter)))) + errors = list( + filter( + lambda e: e.code.startswith("design_") + or e.code.startswith("implementation_"), + set(analysis.check(inter)), + ) + ) errors = sorted(errors, key=lambda e: (e.path, e.line, e.code)) self.assertEqual(len(errors), n_errors) for i in range(n_errors): self.assertEqual(errors[i].code, codes[i]) - self.assertEqual(errors[i].line, lines[i]) - + self.assertEqual(errors[i].line, lines[i]) + def test_ansible_long_statement(self): self.__help_test( "tests/design/ansible/files/long_statement.yml", "tasks", - 1, ["implementation_long_statement"], [16] + 1, + ["implementation_long_statement"], + [16], ) # Tabs @@ -30,65 +38,71 @@ def test_ansible_improper_alignment(self): self.__help_test( "tests/design/ansible/files/improper_alignment.yml", "tasks", - 4, + 4, [ "design_multifaceted_abstraction", - "implementation_improper_alignment", - "implementation_improper_alignment", - "implementation_improper_alignment" - ], [2, 4, 5, 6] + "implementation_improper_alignment", + "implementation_improper_alignment", + "implementation_improper_alignment", + ], + [2, 4, 5, 6], ) def test_ansible_duplicate_block(self): self.__help_test( "tests/design/ansible/files/duplicate_block.yml", "tasks", - 4, + 4, [ - "design_duplicate_block", - "design_duplicate_block", - "design_duplicate_block", - "design_duplicate_block", - ], [2, 10, 25, 33] + "design_duplicate_block", + "design_duplicate_block", + "design_duplicate_block", + "design_duplicate_block", + ], + [2, 10, 25, 33], ) def test_ansible_avoid_comments(self): self.__help_test( "tests/design/ansible/files/avoid_comments.yml", "tasks", - 1, + 1, [ - "design_avoid_comments", - ], [11] + "design_avoid_comments", + ], + [11], ) def test_ansible_long_resource(self): self.__help_test( "tests/design/ansible/files/long_resource.yml", "tasks", - 2, + 2, [ - "design_long_resource", - "design_multifaceted_abstraction", - ], [2, 2] + "design_long_resource", + "design_multifaceted_abstraction", + ], + [2, 2], ) def test_ansible_multifaceted_abstraction(self): self.__help_test( "tests/design/ansible/files/multifaceted_abstraction.yml", "tasks", - 1, + 1, [ - "design_multifaceted_abstraction", - ], [2, 2] + "design_multifaceted_abstraction", + ], + [2, 2], ) def test_ansible_too_many_variables(self): self.__help_test( "tests/design/ansible/files/too_many_variables.yml", "script", - 1, + 1, [ - "implementation_too_many_variables", - ], [-1] + "implementation_too_many_variables", + ], + [-1], ) diff --git a/glitch/tests/design/chef/test_design.py b/glitch/tests/design/chef/test_design.py index 7cc29d91..131d251d 100644 --- a/glitch/tests/design/chef/test_design.py +++ b/glitch/tests/design/chef/test_design.py @@ -4,89 +4,101 @@ from glitch.parsers.chef import ChefParser from glitch.tech import Tech + class TestDesign(unittest.TestCase): def __help_test(self, path, n_errors, codes, lines): parser = ChefParser() inter = parser.parse(path, "script", False) analysis = DesignVisitor(Tech.chef) analysis.config("tests/design/chef/design_chef.ini") - errors = list(filter(lambda e: e.code.startswith('design_') - or e.code.startswith('implementation_'), set(analysis.check(inter)))) + errors = list( + filter( + lambda e: e.code.startswith("design_") + or e.code.startswith("implementation_"), + set(analysis.check(inter)), + ) + ) errors = sorted(errors, key=lambda e: (e.path, e.line, e.code)) self.assertEqual(len(errors), n_errors) for i in range(n_errors): self.assertEqual(errors[i].code, codes[i]) - self.assertEqual(errors[i].line, lines[i]) - + self.assertEqual(errors[i].line, lines[i]) + def test_chef_long_statement(self): self.__help_test( "tests/design/chef/files/long_statement.rb", - 1, ["implementation_long_statement"], [6] + 1, + ["implementation_long_statement"], + [6], ) def test_chef_improper_alignment(self): self.__help_test( "tests/design/chef/files/improper_alignment.rb", - 1, - [ - "implementation_improper_alignment" - ], [1] + 1, + ["implementation_improper_alignment"], + [1], ) def test_chef_duplicate_block(self): self.__help_test( "tests/design/chef/files/duplicate_block.rb", - 4, + 4, [ - "design_duplicate_block", - "implementation_long_statement", - "design_duplicate_block", - "implementation_long_statement", - ], [3, 4, 9, 10] + "design_duplicate_block", + "implementation_long_statement", + "design_duplicate_block", + "implementation_long_statement", + ], + [3, 4, 9, 10], ) def test_chef_avoid_comments(self): self.__help_test( "tests/design/chef/files/avoid_comments.rb", - 1, + 1, [ - "design_avoid_comments", - ], [7] + "design_avoid_comments", + ], + [7], ) def test_chef_long_resource(self): self.__help_test( "tests/design/chef/files/long_resource.rb", - 1, + 1, [ - "design_long_resource", - ], [1] + "design_long_resource", + ], + [1], ) def test_chef_multifaceted_abstraction(self): self.__help_test( "tests/design/chef/files/multifaceted_abstraction.rb", - 1, + 1, [ - "design_multifaceted_abstraction", - ], [1] + "design_multifaceted_abstraction", + ], + [1], ) def test_chef_misplaced_attribute(self): self.__help_test( "tests/design/chef/files/misplaced_attribute.rb", - 1, + 1, [ - "design_misplaced_attribute", - ], [1] + "design_misplaced_attribute", + ], + [1], ) def test_chef_too_many_variables(self): self.__help_test( "tests/design/chef/files/too_many_variables.rb", - 1, + 1, [ - "implementation_too_many_variables", - ], [-1] + "implementation_too_many_variables", + ], + [-1], ) - diff --git a/glitch/tests/design/docker/test_design.py b/glitch/tests/design/docker/test_design.py index b4a20305..923a9de4 100644 --- a/glitch/tests/design/docker/test_design.py +++ b/glitch/tests/design/docker/test_design.py @@ -4,14 +4,20 @@ from glitch.parsers.docker import DockerParser from glitch.tech import Tech + class TestDesign(unittest.TestCase): def __help_test(self, path, n_errors, codes, lines): parser = DockerParser() inter = parser.parse(path, "script", False) analysis = DesignVisitor(Tech.docker) analysis.config("configs/default.ini") - errors = list(filter(lambda e: e.code.startswith('design_') - or e.code.startswith('implementation_'), set(analysis.check(inter)))) + errors = list( + filter( + lambda e: e.code.startswith("design_") + or e.code.startswith("implementation_"), + set(analysis.check(inter)), + ) + ) errors = sorted(errors, key=lambda e: (e.path, e.line, e.code)) self.assertEqual(len(errors), n_errors) for i in range(n_errors): @@ -21,7 +27,9 @@ def __help_test(self, path, n_errors, codes, lines): def test_docker_long_statement(self): self.__help_test( "tests/design/docker/files/long_statement.Dockerfile", - 1, ["implementation_long_statement"], [4] + 1, + ["implementation_long_statement"], + [4], ) def test_docker_improper_alignment(self): @@ -43,7 +51,8 @@ def test_docker_duplicate_block(self): [ "design_duplicate_block", "design_duplicate_block", - ], [1, 9] + ], + [1, 9], ) def test_docker_avoid_comments(self): @@ -52,7 +61,8 @@ def test_docker_avoid_comments(self): 1, [ "design_avoid_comments", - ], [1] + ], + [1], ) def test_docker_too_many_variables(self): @@ -61,5 +71,6 @@ def test_docker_too_many_variables(self): 1, [ "implementation_too_many_variables", - ], [1] + ], + [1], ) diff --git a/glitch/tests/design/puppet/test_design.py b/glitch/tests/design/puppet/test_design.py index 2713f242..07058194 100644 --- a/glitch/tests/design/puppet/test_design.py +++ b/glitch/tests/design/puppet/test_design.py @@ -4,96 +4,107 @@ from glitch.parsers.puppet import PuppetParser from glitch.tech import Tech + class TestDesign(unittest.TestCase): def __help_test(self, path, n_errors, codes, lines): parser = PuppetParser() inter = parser.parse(path, "script", False) analysis = DesignVisitor(Tech.puppet) analysis.config("tests/design/puppet/design_puppet.ini") - errors = list(filter(lambda e: e.code.startswith('design_') - or e.code.startswith('implementation_'), set(analysis.check(inter)))) + errors = list( + filter( + lambda e: e.code.startswith("design_") + or e.code.startswith("implementation_"), + set(analysis.check(inter)), + ) + ) errors = sorted(errors, key=lambda e: (e.path, e.line, e.code)) self.assertEqual(len(errors), n_errors) for i in range(n_errors): self.assertEqual(errors[i].code, codes[i]) - self.assertEqual(errors[i].line, lines[i]) - + self.assertEqual(errors[i].line, lines[i]) + def test_puppet_long_statement(self): self.__help_test( "tests/design/puppet/files/long_statement.pp", - 1, ["implementation_long_statement"], [6] + 1, + ["implementation_long_statement"], + [6], ) def test_puppet_improper_alignment(self): self.__help_test( "tests/design/puppet/files/improper_alignment.pp", - 1, - [ - "implementation_improper_alignment" - ], [1] + 1, + ["implementation_improper_alignment"], + [1], ) def test_puppet_duplicate_block(self): self.__help_test( "tests/design/puppet/files/duplicate_block.pp", - 2, + 2, [ - "design_duplicate_block", - "design_duplicate_block", - ], [1, 10] + "design_duplicate_block", + "design_duplicate_block", + ], + [1, 10], ) def test_puppet_avoid_comments(self): self.__help_test( "tests/design/puppet/files/avoid_comments.pp", - 1, + 1, [ - "design_avoid_comments", - ], [5] + "design_avoid_comments", + ], + [5], ) def test_puppet_long_resource(self): self.__help_test( "tests/design/puppet/files/long_resource.pp", - 1, + 1, [ - "design_long_resource", - ], [1] + "design_long_resource", + ], + [1], ) def test_puppet_multifaceted_abstraction(self): self.__help_test( "tests/design/puppet/files/multifaceted_abstraction.pp", - 2, - [ - "design_multifaceted_abstraction", - "implementation_long_statement" - ], [1, 2] + 2, + ["design_multifaceted_abstraction", "implementation_long_statement"], + [1, 2], ) def test_puppet_unguarded_variable(self): self.__help_test( "tests/design/puppet/files/unguarded_variable.pp", - 1, + 1, [ - "implementation_unguarded_variable", - ], [12] + "implementation_unguarded_variable", + ], + [12], ) def test_puppet_misplaced_attribute(self): self.__help_test( "tests/design/puppet/files/misplaced_attribute.pp", - 1, + 1, [ - "design_misplaced_attribute", - ], [1] + "design_misplaced_attribute", + ], + [1], ) - + def test_puppet_too_many_variables(self): self.__help_test( "tests/design/puppet/files/too_many_variables.pp", - 1, + 1, [ - "implementation_too_many_variables", - ], [1] + "implementation_too_many_variables", + ], + [1], ) diff --git a/glitch/tests/design/terraform/test_design.py b/glitch/tests/design/terraform/test_design.py index c11be9ba..7d7d07bd 100644 --- a/glitch/tests/design/terraform/test_design.py +++ b/glitch/tests/design/terraform/test_design.py @@ -4,60 +4,70 @@ from glitch.parsers.terraform import TerraformParser from glitch.tech import Tech + class TestDesign(unittest.TestCase): def __help_test(self, path, n_errors, codes, lines): parser = TerraformParser() inter = parser.parse(path, "script", False) analysis = DesignVisitor(Tech.terraform) analysis.config("configs/default.ini") - errors = list(filter(lambda e: e.code.startswith('design_') - or e.code.startswith('implementation_'), set(analysis.check(inter)))) + errors = list( + filter( + lambda e: e.code.startswith("design_") + or e.code.startswith("implementation_"), + set(analysis.check(inter)), + ) + ) errors = sorted(errors, key=lambda e: (e.path, e.line, e.code)) self.assertEqual(len(errors), n_errors) for i in range(n_errors): self.assertEqual(errors[i].code, codes[i]) - self.assertEqual(errors[i].line, lines[i]) - + self.assertEqual(errors[i].line, lines[i]) + def test_terraform_long_statement(self): self.__help_test( "tests/design/terraform/files/long_statement.tf", - 1, ["implementation_long_statement"], [6] + 1, + ["implementation_long_statement"], + [6], ) def test_terraform_improper_alignment(self): self.__help_test( "tests/design/terraform/files/improper_alignment.tf", - 1, - [ - "implementation_improper_alignment" - ], [1] + 1, + ["implementation_improper_alignment"], + [1], ) def test_terraform_duplicate_block(self): self.__help_test( "tests/design/terraform/files/duplicate_block.tf", - 2, + 2, [ - "design_duplicate_block", - "design_duplicate_block", - ], [1, 10] + "design_duplicate_block", + "design_duplicate_block", + ], + [1, 10], ) def test_terraform_avoid_comments(self): self.__help_test( "tests/design/terraform/files/avoid_comments.tf", - 2, + 2, [ "design_avoid_comments", - "design_avoid_comments", - ], [2, 8] + "design_avoid_comments", + ], + [2, 8], ) def test_terraform_too_many_variables(self): self.__help_test( "tests/design/terraform/files/too_many_variables.tf", - 1, + 1, [ - "implementation_too_many_variables", - ], [-1] + "implementation_too_many_variables", + ], + [-1], ) diff --git a/glitch/tests/hierarchical/test_parsers.py b/glitch/tests/hierarchical/test_parsers.py index 96866ae9..5ba484e3 100644 --- a/glitch/tests/hierarchical/test_parsers.py +++ b/glitch/tests/hierarchical/test_parsers.py @@ -3,13 +3,16 @@ from glitch.parsers.chef import ChefParser from glitch.parsers.puppet import PuppetParser + class TestAnsible(unittest.TestCase): def __test_parse_vars(self, path, vars): with open(path, "r") as file: - unitblock = AnsibleParser._AnsibleParser__parse_vars_file(self, "test", file) + unitblock = AnsibleParser._AnsibleParser__parse_vars_file( + self, "test", file + ) self.assertEqual(str(unitblock.variables), vars) file.close() - + def __test_parse_attributes(self, path, attributes): with open(path, "r") as file: unitblock = AnsibleParser._AnsibleParser__parse_playbook(self, "test", file) @@ -20,10 +23,12 @@ def __test_parse_attributes(self, path, attributes): def test_hierarchichal_vars(self): vars = "[test[0]:None:[test1[0]:\"['1', '2']\"], test[1]:\"['3', '4']\", test:\"['x', 'y', '23']\", test2[0]:\"['2', '5', '6']\", vars:None:[factorial_of:'5', factorial_value:'1']]" self.__test_parse_vars("tests/hierarchical/ansible/vars.yml", vars) - + def test_hierarchical_attributes(self): attributes = "[hosts:'localhost', debug:None:[msg:'The factorial of 5 is {{ factorial_value }}', seq[0]:None:[test:'something'], seq:\"['y', 'z']\", hash:None:[test1:'1', test2:'2']]]" - self.__test_parse_attributes("tests/hierarchical/ansible/attributes.yml", attributes) + self.__test_parse_attributes( + "tests/hierarchical/ansible/attributes.yml", attributes + ) class TestPuppet(unittest.TestCase): @@ -46,5 +51,5 @@ def test_hierarchical_vars(self): self.__test_parse_vars("tests/hierarchical/chef/vars.rb", vars) -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() diff --git a/glitch/tests/parser/puppet/test_parser.py b/glitch/tests/parser/puppet/test_parser.py index 6d0284e5..0abcb7cc 100644 --- a/glitch/tests/parser/puppet/test_parser.py +++ b/glitch/tests/parser/puppet/test_parser.py @@ -2,16 +2,15 @@ from glitch.parsers.puppet import PuppetParser from glitch.repr.inter import * + class TestPuppetParser(unittest.TestCase): def test_puppet_parser_if(self): - unit_block = PuppetParser().parse_file( - "tests/parser/puppet/files/if.pp", None - ) + unit_block = PuppetParser().parse_file("tests/parser/puppet/files/if.pp", None) assert len(unit_block.statements) == 1 assert isinstance(unit_block.statements[0], ConditionalStatement) # FIXME: the expression should not be a string and or at least should be # equal to the script - assert unit_block.statements[0].condition == "$x==absent" + assert unit_block.statements[0].condition == "$x==absent" assert len(unit_block.statements[0].statements) == 1 assert isinstance(unit_block.statements[0].statements[0], AtomicUnit) assert unit_block.statements[0].else_statement is not None @@ -20,5 +19,3 @@ def test_puppet_parser_if(self): assert isinstance( unit_block.statements[0].else_statement.statements[0], AtomicUnit ) - - \ No newline at end of file diff --git a/glitch/tests/parser/terraform/test_parser.py b/glitch/tests/parser/terraform/test_parser.py index 4ee5b9b1..121e026c 100644 --- a/glitch/tests/parser/terraform/test_parser.py +++ b/glitch/tests/parser/terraform/test_parser.py @@ -1,46 +1,61 @@ import unittest from glitch.parsers.terraform import TerraformParser + class TestTerraform(unittest.TestCase): def __help_test(self, path, attributes): unitblock = TerraformParser().parse_file(path, None) au = unitblock.atomic_units[0] self.assertEqual(str(au.attributes), attributes) - + def __help_test_comments(self, path, comments): unitblock = TerraformParser().parse_file(path, None) self.assertEqual(str(unitblock.comments), comments) def test_terraform_null_value(self): attributes = "[account_id:'']" - self.__help_test("tests/parser/terraform/files/null_value_assign.tf", attributes) + self.__help_test( + "tests/parser/terraform/files/null_value_assign.tf", attributes + ) def test_terraform_empty_string(self): attributes = "[account_id:'']" - self.__help_test("tests/parser/terraform/files/empty_string_assign.tf", attributes) + self.__help_test( + "tests/parser/terraform/files/empty_string_assign.tf", attributes + ) def test_terraform_boolean_value(self): attributes = "[account_id:'True']" - self.__help_test("tests/parser/terraform/files/boolean_value_assign.tf", attributes) + self.__help_test( + "tests/parser/terraform/files/boolean_value_assign.tf", attributes + ) def test_terraform_multiline_string(self): attributes = "[user_data:' #!/bin/bash\\n sudo apt-get update\\n sudo apt-get install -y apache2\\n sudo systemctl start apache2']" - self.__help_test("tests/parser/terraform/files/multiline_string_assign.tf", attributes) + self.__help_test( + "tests/parser/terraform/files/multiline_string_assign.tf", attributes + ) def test_terraform_value_has_variable(self): - attributes = "[access:None:[user_by_email:'${google_service_account.bqowner.email}'], test:'${var.value1}']" - self.__help_test("tests/parser/terraform/files/value_has_variable.tf", attributes) + attributes = "[access:None:[user_by_email:'${google_service_account.bqowner.email}'], test:'${var.value1}']" + self.__help_test( + "tests/parser/terraform/files/value_has_variable.tf", attributes + ) def test_terraform_dict_value(self): - attributes = "[labels:None:[env:'default']]" - self.__help_test("tests/parser/terraform/files/dict_value_assign.tf", attributes) + attributes = "[labels:None:[env:'default']]" + self.__help_test( + "tests/parser/terraform/files/dict_value_assign.tf", attributes + ) def test_terraform_list_value(self): - attributes = "[keys[0]:'value1', keys[1][0]:'1', keys[1][1]:None:[key2:'value2'], keys[2]:None:[key3:'value3']]" - self.__help_test("tests/parser/terraform/files/list_value_assign.tf", attributes) + attributes = "[keys[0]:'value1', keys[1][0]:'1', keys[1][1]:None:[key2:'value2'], keys[2]:None:[key3:'value3']]" + self.__help_test( + "tests/parser/terraform/files/list_value_assign.tf", attributes + ) def test_terraform_dynamic_block(self): - attributes = "[dynamic.setting:None:[content:None:[namespace:'${setting.value[\"namespace\"]}']]]" + attributes = "[dynamic.setting:None:[content:None:[namespace:'${setting.value[\"namespace\"]}']]]" self.__help_test("tests/parser/terraform/files/dynamic_block.tf", attributes) def test_terraform_comments(self): diff --git a/glitch/tests/repair/interactive/test_delta_p.py b/glitch/tests/repair/interactive/test_delta_p.py index a15bd349..fdb98361 100644 --- a/glitch/tests/repair/interactive/test_delta_p.py +++ b/glitch/tests/repair/interactive/test_delta_p.py @@ -135,6 +135,7 @@ def test_delta_p_to_filesystems_default_state(): fss = statement.to_filesystems() assert len(fss) == 1 assert fss[0].state == { - "/root/.ssh/config": - File("0600", "root", "template('fuel/root_ssh_config.erb')") + "/root/.ssh/config": File( + "0600", "root", "template('fuel/root_ssh_config.erb')" + ) } diff --git a/glitch/tests/repair/interactive/test_patch_solver.py b/glitch/tests/repair/interactive/test_patch_solver.py index 0c6d4cef..e7f576fa 100644 --- a/glitch/tests/repair/interactive/test_patch_solver.py +++ b/glitch/tests/repair/interactive/test_patch_solver.py @@ -91,11 +91,11 @@ def setup_patch_solver( def patch_solver_apply( - solver: PatchSolver, - model: ModelRef, - filesystem: FileSystemState, + solver: PatchSolver, + model: ModelRef, + filesystem: FileSystemState, tech: Tech, - n_filesystems: int = 1 + n_filesystems: int = 1, ): solver.apply_patch(model, labeled_script) statement = DeltaPCompiler.compile(labeled_script, tech) @@ -105,7 +105,8 @@ def patch_solver_apply( # TODO: Refactor tests - + + def test_patch_solver_if(): setup_patch_solver( puppet_script_4, PuppetParser(), UnitBlockType.script, Tech.puppet @@ -184,6 +185,7 @@ def test_patch_solver_owner(): patch_solver_apply(solver, model, filesystem, Tech.puppet) + def test_patch_solver_two_files(): setup_patch_solver( puppet_script_3, PuppetParser(), UnitBlockType.script, Tech.puppet @@ -196,7 +198,7 @@ def test_patch_solver_two_files(): assert len(models) == 1 model = models[0] assert model[solver.sum_var] == 6 - + patch_solver_apply(solver, model, filesystem, Tech.puppet) @@ -216,10 +218,7 @@ def test_patch_solver_delete_file(): assert model[solver.unchanged[2]] == 0 assert model[solver.unchanged[3]] == 0 assert model[solver.unchanged[4]] == 0 - assert ( - model[solver.vars["content-1"]] - == UNDEF - ) + assert model[solver.vars["content-1"]] == UNDEF assert model[solver.vars["state-2"]] == "absent" assert model[solver.vars["mode-3"]] == UNDEF assert model[solver.vars["owner-4"]] == UNDEF @@ -274,10 +273,7 @@ def test_patch_solver_mode_ansible(): assert model[solver.vars["state-1"]] == "present" assert model[solver.vars["owner-2"]] == "web_admin" assert model[solver.vars["mode-3"]] == "0777" - assert ( - model[solver.vars["sketched-content-4"]] - == UNDEF - ) + assert model[solver.vars["sketched-content-4"]] == UNDEF patch_solver_apply(solver, model, filesystem, Tech.ansible) @@ -296,4 +292,4 @@ def test_patch_solver_new_attribute_difficult_name(): solver = PatchSolver(statement, filesystem) models = solver.solve() assert len(models) == 1 - patch_solver_apply(solver, models[0], filesystem, Tech.puppet) \ No newline at end of file + patch_solver_apply(solver, models[0], filesystem, Tech.puppet) diff --git a/glitch/tests/repair/interactive/test_tracer_transform.py b/glitch/tests/repair/interactive/test_tracer_transform.py index dc4e1a7d..33f809ce 100644 --- a/glitch/tests/repair/interactive/test_tracer_transform.py +++ b/glitch/tests/repair/interactive/test_tracer_transform.py @@ -3,7 +3,10 @@ import shutil import tempfile -from glitch.repair.interactive.tracer.transform import get_affected_paths, get_file_system_state +from glitch.repair.interactive.tracer.transform import ( + get_affected_paths, + get_file_system_state, +) from glitch.repair.interactive.tracer.model import * from glitch.repair.interactive.filesystem import * @@ -37,6 +40,7 @@ def test_get_affected_paths(): file3 = "" temp_dir = None + @pytest.fixture def setup_file_system(): global dir1, file2, file3, temp_dir diff --git a/glitch/tests/security/ansible/test_security.py b/glitch/tests/security/ansible/test_security.py index f3794793..ed995e66 100644 --- a/glitch/tests/security/ansible/test_security.py +++ b/glitch/tests/security/ansible/test_security.py @@ -4,87 +4,100 @@ from glitch.parsers.ansible import AnsibleParser from glitch.tech import Tech + class TestSecurity(unittest.TestCase): def __help_test(self, path, type, n_errors, codes, lines): parser = AnsibleParser() inter = parser.parse(path, type, False) analysis = SecurityVisitor(Tech.ansible) analysis.config("configs/default.ini") - errors = list(filter(lambda e: e.code.startswith('sec_'), set(analysis.check(inter)))) + errors = list( + filter(lambda e: e.code.startswith("sec_"), set(analysis.check(inter))) + ) errors = sorted(errors, key=lambda e: (e.path, e.line, e.code)) self.assertEqual(len(errors), n_errors) for i in range(n_errors): self.assertEqual(errors[i].code, codes[i]) - self.assertEqual(errors[i].line, lines[i]) - + self.assertEqual(errors[i].line, lines[i]) + def test_ansible_http(self): self.__help_test( - "tests/security/ansible/files/http.yml", - "tasks", - 1, ["sec_https"], [4] + "tests/security/ansible/files/http.yml", "tasks", 1, ["sec_https"], [4] ) def test_ansible_susp_comment(self): self.__help_test( - "tests/security/ansible/files/susp.yml", - "vars", - 1, ["sec_susp_comm"], [9] + "tests/security/ansible/files/susp.yml", "vars", 1, ["sec_susp_comm"], [9] ) def test_ansible_def_admin(self): self.__help_test( "tests/security/ansible/files/admin.yml", "tasks", - 3, ["sec_def_admin", "sec_hard_secr", "sec_hard_user"], [3, 3, 3] + 3, + ["sec_def_admin", "sec_hard_secr", "sec_hard_user"], + [3, 3, 3], ) def test_ansible_empt_pass(self): self.__help_test( "tests/security/ansible/files/empty.yml", "tasks", - 1, ["sec_empty_pass"], [8] + 1, + ["sec_empty_pass"], + [8], ) def test_ansible_weak_crypt(self): self.__help_test( "tests/security/ansible/files/weak_crypt.yml", "tasks", - 2, ["sec_weak_crypt", "sec_weak_crypt"], [4, 7] + 2, + ["sec_weak_crypt", "sec_weak_crypt"], + [4, 7], ) def test_ansible_hard_secr(self): self.__help_test( "tests/security/ansible/files/hard_secr.yml", "tasks", - 4, - ["sec_hard_secr", "sec_hard_user", "sec_hard_pass", "sec_hard_secr"] - , [7, 7, 8, 8] + 4, + ["sec_hard_secr", "sec_hard_user", "sec_hard_pass", "sec_hard_secr"], + [7, 7, 8, 8], ) def test_ansible_invalid_bind(self): self.__help_test( "tests/security/ansible/files/inv_bind.yml", "tasks", - 1, ["sec_invalid_bind"], [7] + 1, + ["sec_invalid_bind"], + [7], ) def test_ansible_int_check(self): self.__help_test( "tests/security/ansible/files/int_check.yml", "tasks", - 1, ["sec_no_int_check"], [5] + 1, + ["sec_no_int_check"], + [5], ) def test_ansible_full_perm(self): self.__help_test( "tests/security/ansible/files/full_permission.yml", "tasks", - 1, ["sec_full_permission_filesystem"], [7] + 1, + ["sec_full_permission_filesystem"], + [7], ) def test_ansible_obs_command(self): self.__help_test( "tests/security/ansible/files/obs_command.yml", "tasks", - 1, ["sec_obsolete_command"], [2] + 1, + ["sec_obsolete_command"], + [2], ) diff --git a/glitch/tests/security/chef/test_security.py b/glitch/tests/security/chef/test_security.py index 2af7dee9..9de30689 100644 --- a/glitch/tests/security/chef/test_security.py +++ b/glitch/tests/security/chef/test_security.py @@ -4,83 +4,81 @@ from glitch.parsers.chef import ChefParser from glitch.tech import Tech + class TestSecurity(unittest.TestCase): def __help_test(self, path, n_errors, codes, lines): parser = ChefParser() inter = parser.parse(path, "script", False) analysis = SecurityVisitor(Tech.chef) analysis.config("configs/default.ini") - errors = list(filter(lambda e: e.code.startswith('sec_'), set(analysis.check(inter)))) + errors = list( + filter(lambda e: e.code.startswith("sec_"), set(analysis.check(inter))) + ) errors = sorted(errors, key=lambda e: (e.path, e.line, e.code)) self.assertEqual(len(errors), n_errors) for i in range(n_errors): self.assertEqual(errors[i].code, codes[i]) - self.assertEqual(errors[i].line, lines[i]) - + self.assertEqual(errors[i].line, lines[i]) + def test_chef_http(self): - self.__help_test( - "tests/security/chef/files/http.rb", - 1, ["sec_https"], [3] - ) + self.__help_test("tests/security/chef/files/http.rb", 1, ["sec_https"], [3]) def test_chef_susp_comment(self): - self.__help_test( - "tests/security/chef/files/susp.rb", - 1, ["sec_susp_comm"], [1] - ) + self.__help_test("tests/security/chef/files/susp.rb", 1, ["sec_susp_comm"], [1]) def test_chef_def_admin(self): self.__help_test( "tests/security/chef/files/admin.rb", - 3, ["sec_def_admin", "sec_hard_secr", "sec_hard_user"], [8, 8, 8] + 3, + ["sec_def_admin", "sec_hard_secr", "sec_hard_user"], + [8, 8, 8], ) def test_chef_empt_pass(self): self.__help_test( - "tests/security/chef/files/empty.rb", - 1, ["sec_empty_pass"], [1] + "tests/security/chef/files/empty.rb", 1, ["sec_empty_pass"], [1] ) def test_chef_weak_crypt(self): self.__help_test( - "tests/security/chef/files/weak_crypt.rb", - 1, ["sec_weak_crypt"], [4] + "tests/security/chef/files/weak_crypt.rb", 1, ["sec_weak_crypt"], [4] ) def test_chef_hard_secr(self): self.__help_test( "tests/security/chef/files/hard_secr.rb", - 2, - ["sec_hard_pass", "sec_hard_secr"] - , [8, 8] + 2, + ["sec_hard_pass", "sec_hard_secr"], + [8, 8], ) def test_chef_invalid_bind(self): self.__help_test( - "tests/security/chef/files/inv_bind.rb", - 1, ["sec_invalid_bind"], [7] + "tests/security/chef/files/inv_bind.rb", 1, ["sec_invalid_bind"], [7] ) def test_chef_int_check(self): self.__help_test( - "tests/security/chef/files/int_check.rb", - 1, ["sec_no_int_check"], [1] + "tests/security/chef/files/int_check.rb", 1, ["sec_no_int_check"], [1] ) def test_chef_missing_default(self): self.__help_test( "tests/security/chef/files/missing_default.rb", - 1, ["sec_no_default_switch"], [2] + 1, + ["sec_no_default_switch"], + [2], ) def test_chef_full_permission(self): self.__help_test( "tests/security/chef/files/full_permission.rb", - 1, ["sec_full_permission_filesystem"], [3] + 1, + ["sec_full_permission_filesystem"], + [3], ) def test_chef_obs_command(self): self.__help_test( - "tests/security/chef/files/obs_command.rb", - 1, ["sec_obsolete_command"], [2] + "tests/security/chef/files/obs_command.rb", 1, ["sec_obsolete_command"], [2] ) diff --git a/glitch/tests/security/docker/test_security.py b/glitch/tests/security/docker/test_security.py index 31bef117..960a740a 100644 --- a/glitch/tests/security/docker/test_security.py +++ b/glitch/tests/security/docker/test_security.py @@ -13,7 +13,9 @@ def __help_test(self, path, n_errors, codes, lines): inter = parser.parse(path, UnitBlockType.script, False) analysis = SecurityVisitor(Tech.docker) analysis.config("configs/default.ini") - errors = list(filter(lambda e: e.code.startswith('sec_'), set(analysis.check(inter)))) + errors = list( + filter(lambda e: e.code.startswith("sec_"), set(analysis.check(inter))) + ) errors = sorted(errors, key=lambda e: (e.path, e.line, e.code)) self.assertEqual(len(errors), n_errors) for i in range(n_errors): @@ -28,66 +30,79 @@ def tearDown(self) -> None: def test_docker_admin(self): self.__help_test( "tests/security/docker/files/admin.Dockerfile", - 2, ['sec_def_admin', 'sec_def_admin'], [2, 4] + 2, + ["sec_def_admin", "sec_def_admin"], + [2, 4], ) def test_docker_empty(self): self.__help_test( - "tests/security/docker/files/empty.Dockerfile", - 1, ["sec_empty_pass"], [4] + "tests/security/docker/files/empty.Dockerfile", 1, ["sec_empty_pass"], [4] ) pass def test_docker_full_permission(self): self.__help_test( "tests/security/docker/files/full_permission.Dockerfile", - 1, ['sec_full_permission_filesystem'], [3] + 1, + ["sec_full_permission_filesystem"], + [3], ) def test_docker_hard_secret(self): self.__help_test( "tests/security/docker/files/hard_secr.Dockerfile", - 2, ["sec_hard_pass", "sec_hard_secr"], [3, 3] + 2, + ["sec_hard_pass", "sec_hard_secr"], + [3, 3], ) def test_docker_http(self): self.__help_test( - "tests/security/docker/files/http.Dockerfile", - 1, ["sec_https"], [5] + "tests/security/docker/files/http.Dockerfile", 1, ["sec_https"], [5] ) def test_docker_int_check(self): self.__help_test( "tests/security/docker/files/int_check.Dockerfile", - 1, ["sec_no_int_check"], [4] + 1, + ["sec_no_int_check"], + [4], ) def test_docker_inv_bind(self): self.__help_test( "tests/security/docker/files/inv_bind.Dockerfile", - 1, ["sec_invalid_bind"], [4] + 1, + ["sec_invalid_bind"], + [4], ) def test_docker_non_official_image(self): self.__help_test( "tests/security/docker/files/non_off_image.Dockerfile", - 1, ["sec_non_official_image"], [1] + 1, + ["sec_non_official_image"], + [1], ) def test_docker_obs_command(self): self.__help_test( "tests/security/docker/files/obs_command.Dockerfile", - 1, ["sec_obsolete_command"], [4] + 1, + ["sec_obsolete_command"], + [4], ) def test_docker_susp(self): self.__help_test( - "tests/security/docker/files/susp.Dockerfile", - 1, ["sec_susp_comm"], [3] + "tests/security/docker/files/susp.Dockerfile", 1, ["sec_susp_comm"], [3] ) def test_docker_weak_crypt(self): self.__help_test( "tests/security/docker/files/weak_crypt.Dockerfile", - 1, ["sec_weak_crypt"], [8] + 1, + ["sec_weak_crypt"], + [8], ) diff --git a/glitch/tests/security/puppet/test_security.py b/glitch/tests/security/puppet/test_security.py index 02fb41bd..199c2e9b 100644 --- a/glitch/tests/security/puppet/test_security.py +++ b/glitch/tests/security/puppet/test_security.py @@ -4,83 +4,86 @@ from glitch.parsers.puppet import PuppetParser from glitch.tech import Tech + class TestSecurity(unittest.TestCase): def __help_test(self, path, n_errors, codes, lines): parser = PuppetParser() inter = parser.parse(path, "script", False) analysis = SecurityVisitor(Tech.puppet) analysis.config("configs/default.ini") - errors = list(filter(lambda e: e.code.startswith('sec_'), set(analysis.check(inter)))) + errors = list( + filter(lambda e: e.code.startswith("sec_"), set(analysis.check(inter))) + ) errors = sorted(errors, key=lambda e: (e.path, e.line, e.code)) self.assertEqual(len(errors), n_errors) for i in range(n_errors): self.assertEqual(errors[i].code, codes[i]) - self.assertEqual(errors[i].line, lines[i]) - + self.assertEqual(errors[i].line, lines[i]) + def test_puppet_http(self): - self.__help_test( - "tests/security/puppet/files/http.pp", - 1, ["sec_https"], [2] - ) + self.__help_test("tests/security/puppet/files/http.pp", 1, ["sec_https"], [2]) def test_puppet_susp_comment(self): self.__help_test( - "tests/security/puppet/files/susp.pp", - 1, ["sec_susp_comm"], [19] + "tests/security/puppet/files/susp.pp", 1, ["sec_susp_comm"], [19] ) def test_puppet_def_admin(self): self.__help_test( "tests/security/puppet/files/admin.pp", - 3, ["sec_def_admin", "sec_hard_secr", "sec_hard_user"], [7, 7, 7] + 3, + ["sec_def_admin", "sec_hard_secr", "sec_hard_user"], + [7, 7, 7], ) def test_puppet_empt_pass(self): self.__help_test( - "tests/security/puppet/files/empty.pp", - 1, ["sec_empty_pass"], [1] + "tests/security/puppet/files/empty.pp", 1, ["sec_empty_pass"], [1] ) def test_puppet_weak_crypt(self): self.__help_test( - "tests/security/puppet/files/weak_crypt.pp", - 1, ["sec_weak_crypt"], [12] + "tests/security/puppet/files/weak_crypt.pp", 1, ["sec_weak_crypt"], [12] ) def test_puppet_hard_secr(self): self.__help_test( "tests/security/puppet/files/hard_secr.pp", - 2, - ["sec_hard_pass", "sec_hard_secr"] - , [2, 2] + 2, + ["sec_hard_pass", "sec_hard_secr"], + [2, 2], ) def test_puppet_invalid_bind(self): self.__help_test( - "tests/security/puppet/files/inv_bind.pp", - 1, ["sec_invalid_bind"], [12] + "tests/security/puppet/files/inv_bind.pp", 1, ["sec_invalid_bind"], [12] ) def test_puppet_int_check(self): self.__help_test( - "tests/security/puppet/files/int_check.pp", - 1, ["sec_no_int_check"], [5] + "tests/security/puppet/files/int_check.pp", 1, ["sec_no_int_check"], [5] ) def test_puppet_missing_default(self): self.__help_test( "tests/security/puppet/files/missing_default.pp", - 2, ["sec_no_default_switch", "sec_no_default_switch"], [2, 7] + 2, + ["sec_no_default_switch", "sec_no_default_switch"], + [2, 7], ) def test_puppet_full_perm(self): self.__help_test( "tests/security/puppet/files/full_permission.pp", - 1, ["sec_full_permission_filesystem"], [4] + 1, + ["sec_full_permission_filesystem"], + [4], ) def test_puppet_obs_command(self): self.__help_test( "tests/security/puppet/files/obs_command.pp", - 1, ["sec_obsolete_command"], [2] + 1, + ["sec_obsolete_command"], + [2], ) diff --git a/glitch/tests/security/terraform/test_security.py b/glitch/tests/security/terraform/test_security.py index f9df55d9..cb89d44c 100644 --- a/glitch/tests/security/terraform/test_security.py +++ b/glitch/tests/security/terraform/test_security.py @@ -4,62 +4,62 @@ from glitch.parsers.terraform import TerraformParser from glitch.tech import Tech + class TestSecurity(unittest.TestCase): def __help_test(self, path, n_errors, codes, lines): parser = TerraformParser() inter = parser.parse(path, "script", False) analysis = SecurityVisitor(Tech.terraform) analysis.config("configs/terraform.ini") - errors = list(filter(lambda e: e.code.startswith('sec_'), set(analysis.check(inter)))) + errors = list( + filter(lambda e: e.code.startswith("sec_"), set(analysis.check(inter))) + ) errors = sorted(errors, key=lambda e: (e.path, e.line, e.code)) self.assertEqual(len(errors), n_errors) for i in range(n_errors): self.assertEqual(errors[i].code, codes[i]) - self.assertEqual(errors[i].line, lines[i]) + self.assertEqual(errors[i].line, lines[i]) # testing previous implemented code smells def test_terraform_http(self): self.__help_test( - "tests/security/terraform/files/http.tf", - 1, ["sec_https"], [2] + "tests/security/terraform/files/http.tf", 1, ["sec_https"], [2] ) def test_terraform_susp_comment(self): self.__help_test( - "tests/security/terraform/files/susp.tf", - 1, ["sec_susp_comm"], [8] + "tests/security/terraform/files/susp.tf", 1, ["sec_susp_comm"], [8] ) def test_terraform_def_admin(self): self.__help_test( "tests/security/terraform/files/admin.tf", - 3, ["sec_def_admin", "sec_hard_secr", "sec_hard_user"], [2, 2, 2] + 3, + ["sec_def_admin", "sec_hard_secr", "sec_hard_user"], + [2, 2, 2], ) def test_terraform_empt_pass(self): self.__help_test( - "tests/security/terraform/files/empty.tf", - 1, ["sec_empty_pass"], [5] + "tests/security/terraform/files/empty.tf", 1, ["sec_empty_pass"], [5] ) def test_terraform_weak_crypt(self): self.__help_test( - "tests/security/terraform/files/weak_crypt.tf", - 1, ["sec_weak_crypt"], [4] + "tests/security/terraform/files/weak_crypt.tf", 1, ["sec_weak_crypt"], [4] ) def test_terraform_hard_secr(self): self.__help_test( "tests/security/terraform/files/hard_secr.tf", - 2, - ["sec_hard_pass", "sec_hard_secr"] - , [5, 5] + 2, + ["sec_hard_pass", "sec_hard_secr"], + [5, 5], ) def test_terraform_invalid_bind(self): self.__help_test( - "tests/security/terraform/files/inv_bind.tf", - 1, ["sec_invalid_bind"], [19] + "tests/security/terraform/files/inv_bind.tf", 1, ["sec_invalid_bind"], [19] ) # testing new implemented code smells, or previous ones with new rules for Terraform @@ -67,936 +67,1433 @@ def test_terraform_invalid_bind(self): def test_terraform_insecure_access_control(self): self.__help_test( "tests/security/terraform/files/insecure-access-control/access-to-bigquery-dataset.tf", - 1, ["sec_access_control"], [3] + 1, + ["sec_access_control"], + [3], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/aks-ip-ranges-enabled.tf", - 1, ["sec_access_control"], [1] + 1, + ["sec_access_control"], + [1], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/associated-access-block-to-s3-bucket.tf", - 1, ["sec_access_control"], [1] + 1, + ["sec_access_control"], + [1], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/aws-database-instance-publicly-accessible.tf", - 2, ["sec_access_control", "sec_access_control"], [2, 18] + 2, + ["sec_access_control", "sec_access_control"], + [2, 18], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/aws-sqs-no-wildcards-in-policy.tf", - 1, ["sec_access_control"], [4] + 1, + ["sec_access_control"], + [4], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/azure-authorization-wildcard-action.tf", - 1, ["sec_access_control"], [7] + 1, + ["sec_access_control"], + [7], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/azure-container-use-rbac-permissions.tf", - 1, ["sec_access_control"], [2] + 1, + ["sec_access_control"], + [2], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/azure-database-not-publicly-accessible.tf", - 2, ["sec_access_control", "sec_access_control"], [1, 6] + 2, + ["sec_access_control", "sec_access_control"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/bucket-public-read-acl.tf", - 3, ["sec_access_control", "sec_access_control", "sec_access_control"], [1, 8, 25] + 3, + ["sec_access_control", "sec_access_control", "sec_access_control"], + [1, 8, 25], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/cidr-range-public-access-eks-cluster.tf", - 1, ["sec_access_control"], [1] + 1, + ["sec_access_control"], + [1], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/cross-db-ownership-chaining.tf", - 3, ["sec_access_control", "sec_access_control", "sec_access_control"], [1, 50, 97] + 3, + ["sec_access_control", "sec_access_control", "sec_access_control"], + [1, 50, 97], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/data-factory-public-access.tf", - 2, ["sec_access_control", "sec_access_control"], [1, 5] + 2, + ["sec_access_control", "sec_access_control"], + [1, 5], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/google-compute-no-default-service-account.tf", - 2, ["sec_access_control", "sec_access_control"], [1, 19] + 2, + ["sec_access_control", "sec_access_control"], + [1, 19], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/google-gke-use-rbac-permissions.tf", - 1, ["sec_access_control"], [17] + 1, + ["sec_access_control"], + [17], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/google-storage-enable-ubla.tf", - 2, ["sec_access_control", "sec_access_control"], [1, 8] + 2, + ["sec_access_control", "sec_access_control"], + [1, 8], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/google-storage-no-public-access.tf", - 3, ["sec_access_control", "sec_access_control", "sec_access_control"], [4, 10, 22] + 3, + ["sec_access_control", "sec_access_control", "sec_access_control"], + [4, 10, 22], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/mq-broker-publicly-exposed.tf", - 1, ["sec_access_control"], [2] + 1, + ["sec_access_control"], + [2], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/prevent-client-disable-encryption.tf", - 1, ["sec_access_control"], [13] + 1, + ["sec_access_control"], + [13], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/private-cluster-nodes.tf", - 2, ["sec_access_control", "sec_access_control"], [1, 19] + 2, + ["sec_access_control", "sec_access_control"], + [1, 19], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/public-access-eks-cluster.tf", - 1, ["sec_access_control"], [10] + 1, + ["sec_access_control"], + [10], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/public-access-policy.tf", - 1, ["sec_access_control"], [4] + 1, + ["sec_access_control"], + [4], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/public-github-repo.tf", - 3, ["sec_access_control", "sec_access_control", "sec_access_control"], [1, 6, 18] + 3, + ["sec_access_control", "sec_access_control", "sec_access_control"], + [1, 6, 18], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/s3-access-through-acl.tf", - 1, ["sec_access_control"], [7] + 1, + ["sec_access_control"], + [7], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/s3-block-public-acl.tf", - 2, ["sec_access_control", "sec_access_control"], [1, 10] + 2, + ["sec_access_control", "sec_access_control"], + [1, 10], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/s3-block-public-policy.tf", - 2, ["sec_access_control", "sec_access_control"], [1, 11] + 2, + ["sec_access_control", "sec_access_control"], + [1, 11], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/s3-ignore-public-acl.tf", - 2, ["sec_access_control", "sec_access_control"], [1, 13] + 2, + ["sec_access_control", "sec_access_control"], + [1, 13], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/s3-restrict-public-bucket.tf", - 2, ["sec_access_control", "sec_access_control"], [1, 12] + 2, + ["sec_access_control", "sec_access_control"], + [1, 12], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/specify-source-lambda-permission.tf", - 1, ["sec_access_control"], [1] + 1, + ["sec_access_control"], + [1], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/storage-containers-public-access.tf", - 1, ["sec_access_control"], [26] + 1, + ["sec_access_control"], + [26], ) self.__help_test( "tests/security/terraform/files/insecure-access-control/unauthorized-access-api-gateway-methods.tf", - 2, ["sec_access_control", "sec_access_control"], [37, 44] + 2, + ["sec_access_control", "sec_access_control"], + [37, 44], ) def test_terraform_invalid_ip_binding(self): self.__help_test( "tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-egress-sgr.tf", - 2, ["sec_invalid_bind", "sec_invalid_bind"], [5, 20] + 2, + ["sec_invalid_bind", "sec_invalid_bind"], + [5, 20], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-acl.tf", - 1, ["sec_invalid_bind"], [7] + 1, + ["sec_invalid_bind"], + [7], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/aws-ec2-vpc-no-public-ingress-sgr.tf", - 2, ["sec_invalid_bind", "sec_invalid_bind"], [4, 17] + 2, + ["sec_invalid_bind", "sec_invalid_bind"], + [4, 17], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-egress.tf", - 1, ["sec_invalid_bind"], [3] + 1, + ["sec_invalid_bind"], + [3], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/azure-network-no-public-ingress.tf", - 1, ["sec_invalid_bind"], [3] + 1, + ["sec_invalid_bind"], + [3], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/cloud-sql-database-publicly-exposed.tf", - 1, ["sec_invalid_bind"], [14] + 1, + ["sec_invalid_bind"], + [14], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/compute-firewall-inbound-rule-public-ip.tf", - 1, ["sec_invalid_bind"], [9] + 1, + ["sec_invalid_bind"], + [9], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/compute-firewall-outbound-rule-public-ip.tf", - 1, ["sec_invalid_bind"], [9] + 1, + ["sec_invalid_bind"], + [9], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/eks-cluster-open-cidr-range.tf", - 1, ["sec_invalid_bind"], [11] + 1, + ["sec_invalid_bind"], + [11], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/gke-control-plane-publicly-accessible.tf", - 1, ["sec_invalid_bind"], [8] + 1, + ["sec_invalid_bind"], + [8], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-egress.tf", - 1, ["sec_invalid_bind"], [8] + 1, + ["sec_invalid_bind"], + [8], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/openstack-networking-no-public-ingress.tf", - 1, ["sec_invalid_bind"], [8] + 1, + ["sec_invalid_bind"], + [8], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/public-egress-network-policy.tf", - 1, ["sec_invalid_bind"], [27] + 1, + ["sec_invalid_bind"], + [27], ) self.__help_test( "tests/security/terraform/files/invalid-ip-binding/public-ingress-network-policy.tf", - 1, ["sec_invalid_bind"], [27] + 1, + ["sec_invalid_bind"], + [27], ) def test_terraform_disabled_authentication(self): self.__help_test( "tests/security/terraform/files/disabled-authentication/azure-app-service-authentication-activated.tf", - 2, ["sec_authentication", "sec_authentication"], [1, 11] + 2, + ["sec_authentication", "sec_authentication"], + [1, 11], ) self.__help_test( "tests/security/terraform/files/disabled-authentication/contained-database-disabled.tf", - 1, ["sec_authentication"], [1] + 1, + ["sec_authentication"], + [1], ) self.__help_test( "tests/security/terraform/files/disabled-authentication/disable-password-authentication.tf", - 3, ["sec_authentication", "sec_authentication", "sec_authentication"], [2, 13, 18] + 3, + ["sec_authentication", "sec_authentication", "sec_authentication"], + [2, 13, 18], ) self.__help_test( "tests/security/terraform/files/disabled-authentication/gke-basic-auth.tf", - 1, ["sec_authentication"], [4] + 1, + ["sec_authentication"], + [4], ) self.__help_test( "tests/security/terraform/files/disabled-authentication/iam-group-with-mfa.tf", - 2, ["sec_authentication", "sec_authentication"], [7, 53] + 2, + ["sec_authentication", "sec_authentication"], + [7, 53], ) def test_terraform_missing_encryption(self): self.__help_test( "tests/security/terraform/files/missing-encryption/athena-enable-at-rest-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 10] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 10], ) self.__help_test( "tests/security/terraform/files/missing-encryption/aws-codebuild-enable-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [3, 9] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [3, 9], ) self.__help_test( "tests/security/terraform/files/missing-encryption/aws-ecr-encrypted.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 17] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 17], ) self.__help_test( "tests/security/terraform/files/missing-encryption/aws-neptune-at-rest-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 9] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 9], ) self.__help_test( "tests/security/terraform/files/missing-encryption/documentdb-storage-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 9] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 9], ) self.__help_test( "tests/security/terraform/files/missing-encryption/dynamodb-rest-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 9] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 9], ) self.__help_test( "tests/security/terraform/files/missing-encryption/ecs-task-definitions-in-transit-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 29] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 29], ) self.__help_test( "tests/security/terraform/files/missing-encryption/efs-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 6] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/missing-encryption/eks-encryption-secrets-enabled.tf", - 5, ["sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption", - "sec_missing_encryption"], [1, 1, 9, 23, 34] + 5, + [ + "sec_missing_encryption", + "sec_missing_encryption", + "sec_missing_encryption", + "sec_missing_encryption", + "sec_missing_encryption", + ], + [1, 1, 9, 23, 34], ) self.__help_test( "tests/security/terraform/files/missing-encryption/elasticache-enable-at-rest-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 6] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/missing-encryption/elasticache-enable-in-transit-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 7] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/missing-encryption/elasticsearch-domain-encrypted.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 17] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 17], ) self.__help_test( "tests/security/terraform/files/missing-encryption/elasticsearch-in-transit-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 16] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 16], ) self.__help_test( "tests/security/terraform/files/missing-encryption/emr-enable-at-rest-encryption.tf", - 1, ["sec_missing_encryption"], [4] + 1, + ["sec_missing_encryption"], + [4], ) self.__help_test( "tests/security/terraform/files/missing-encryption/emr-enable-in-transit-encryption.tf", - 1, ["sec_missing_encryption"], [4] + 1, + ["sec_missing_encryption"], + [4], ) self.__help_test( "tests/security/terraform/files/missing-encryption/emr-enable-local-disk-encryption.tf", - 1, ["sec_missing_encryption"], [4] + 1, + ["sec_missing_encryption"], + [4], ) self.__help_test( "tests/security/terraform/files/missing-encryption/emr-s3encryption-mode-sse-kms.tf", - 1, ["sec_missing_encryption"], [4] + 1, + ["sec_missing_encryption"], + [4], ) self.__help_test( "tests/security/terraform/files/missing-encryption/enable-cache-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 6] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/missing-encryption/encrypted-ebs-volume.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 7] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/missing-encryption/encrypted-root-block-device.tf", - 4, ["sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption"], - [1, 13, 23, 27] + 4, + [ + "sec_missing_encryption", + "sec_missing_encryption", + "sec_missing_encryption", + "sec_missing_encryption", + ], + [1, 13, 23, 27], ) self.__help_test( "tests/security/terraform/files/missing-encryption/instance-encrypted-block-device.tf", - 1, ["sec_missing_encryption"], [14] + 1, + ["sec_missing_encryption"], + [14], ) self.__help_test( "tests/security/terraform/files/missing-encryption/kinesis-stream-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 6] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/missing-encryption/msk-enable-in-transit-encryption.tf", - 3, ["sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption"], [1, 14, 15] + 3, + [ + "sec_missing_encryption", + "sec_missing_encryption", + "sec_missing_encryption", + ], + [1, 14, 15], ) self.__help_test( "tests/security/terraform/files/missing-encryption/rds-encrypt-cluster-storage-data.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 6] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/missing-encryption/rds-encrypt-instance-storage-data.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 8] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 8], ) self.__help_test( "tests/security/terraform/files/missing-encryption/redshift-cluster-rest-encryption.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [1, 6] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/missing-encryption/unencrypted-s3-bucket.tf", - 2, ["sec_missing_encryption", "sec_missing_encryption"], [25, 64] + 2, + ["sec_missing_encryption", "sec_missing_encryption"], + [25, 64], ) self.__help_test( "tests/security/terraform/files/missing-encryption/workspaces-disk-encryption.tf", - 6, ["sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption", "sec_missing_encryption", - "sec_missing_encryption", "sec_missing_encryption"], [1, 1, 4, 8, 13, 14] + 6, + [ + "sec_missing_encryption", + "sec_missing_encryption", + "sec_missing_encryption", + "sec_missing_encryption", + "sec_missing_encryption", + "sec_missing_encryption", + ], + [1, 1, 4, 8, 13, 14], ) def test_terraform_hard_coded_secrets(self): self.__help_test( "tests/security/terraform/files/hard-coded-secrets/encryption-key-in-plaintext.tf", - 1, ["sec_hard_secr"], [3] + 1, + ["sec_hard_secr"], + [3], ) self.__help_test( "tests/security/terraform/files/hard-coded-secrets/plaintext-password.tf", - 2, ["sec_hard_pass", "sec_hard_secr"], [2, 2] + 2, + ["sec_hard_pass", "sec_hard_secr"], + [2, 2], ) self.__help_test( "tests/security/terraform/files/hard-coded-secrets/plaintext-value-github-actions.tf", - 1, ["sec_hard_secr"], [5] + 1, + ["sec_hard_secr"], + [5], ) self.__help_test( "tests/security/terraform/files/hard-coded-secrets/sensitive-credentials-in-vm-custom-data.tf", - 2, ["sec_hard_pass", "sec_hard_secr"], [3, 3] + 2, + ["sec_hard_pass", "sec_hard_secr"], + [3, 3], ) self.__help_test( "tests/security/terraform/files/hard-coded-secrets/sensitive-data-in-plaintext.tf", - 2, ["sec_hard_pass", "sec_hard_secr"], [8, 8] + 2, + ["sec_hard_pass", "sec_hard_secr"], + [8, 8], ) self.__help_test( "tests/security/terraform/files/hard-coded-secrets/sensitive-data-stored-in-user-data.tf", - 4, ["sec_hard_pass", "sec_hard_secr", "sec_hard_pass", "sec_hard_secr"], [2, 2, 14, 14] + 4, + ["sec_hard_pass", "sec_hard_secr", "sec_hard_pass", "sec_hard_secr"], + [2, 2, 14, 14], ) self.__help_test( "tests/security/terraform/files/hard-coded-secrets/sensitive-environment-variables.tf", - 2, ["sec_hard_pass", "sec_hard_secr"], [2, 2] + 2, + ["sec_hard_pass", "sec_hard_secr"], + [2, 2], ) self.__help_test( "tests/security/terraform/files/hard-coded-secrets/user-data-contains-sensitive-aws-keys.tf", - 1, ["sec_hard_secr"], [9] + 1, + ["sec_hard_secr"], + [9], ) def test_terraform_public_ip(self): self.__help_test( "tests/security/terraform/files/public-ip/google-compute-intance-with-public-ip.tf", - 1, ["sec_public_ip"], [4] + 1, + ["sec_public_ip"], + [4], ) self.__help_test( "tests/security/terraform/files/public-ip/lauch-configuration-public-ip-addr.tf", - 1, ["sec_public_ip"], [2] + 1, + ["sec_public_ip"], + [2], ) self.__help_test( "tests/security/terraform/files/public-ip/oracle-compute-no-public-ip.tf", - 1, ["sec_public_ip"], [3] + 1, + ["sec_public_ip"], + [3], ) self.__help_test( "tests/security/terraform/files/public-ip/subnet-public-ip-address.tf", - 1, ["sec_public_ip"], [3] + 1, + ["sec_public_ip"], + [3], ) - + def test_terraform_use_of_http_without_tls(self): self.__help_test( "tests/security/terraform/files/use-of-http-without-tls/azure-appservice-enforce-https.tf", - 2, ["sec_https", "sec_https"], [1, 8] + 2, + ["sec_https", "sec_https"], + [1, 8], ) self.__help_test( "tests/security/terraform/files/use-of-http-without-tls/azure-storage-enforce-https.tf", - 1, ["sec_https"], [2] + 1, + ["sec_https"], + [2], ) self.__help_test( "tests/security/terraform/files/use-of-http-without-tls/cloudfront-enforce-https.tf", - 2, ["sec_https", "sec_https"], [1, 13] + 2, + ["sec_https", "sec_https"], + [1, 13], ) self.__help_test( "tests/security/terraform/files/use-of-http-without-tls/digitalocean-compute-enforce-https.tf", - 1, ["sec_https"], [7] + 1, + ["sec_https"], + [7], ) self.__help_test( "tests/security/terraform/files/use-of-http-without-tls/elastic-search-enforce-https.tf", - 2, ["sec_https", "sec_https"], [1, 19] + 2, + ["sec_https", "sec_https"], + [1, 19], ) self.__help_test( "tests/security/terraform/files/use-of-http-without-tls/elb-use-plain-http.tf", - 2, ["sec_https", "sec_https"], [1, 6] + 2, + ["sec_https", "sec_https"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/use-of-http-without-tls/aws-ssm-avoid-leaks-via-http.tf", - 1, ["sec_https"], [8] + 1, + ["sec_https"], + [8], ) def test_terraform_ssl_tls_mtls_policy(self): self.__help_test( "tests/security/terraform/files/ssl-tls-mtls-policy/api-gateway-secure-tls-policy.tf", - 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 5] + 2, + ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], + [1, 5], ) self.__help_test( "tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-require-client-cert.tf", - 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 9] + 2, + ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], + [1, 9], ) self.__help_test( "tests/security/terraform/files/ssl-tls-mtls-policy/azure-appservice-secure-tls-policy.tf", - 1, ["sec_ssl_tls_policy"], [3] + 1, + ["sec_ssl_tls_policy"], + [3], ) self.__help_test( "tests/security/terraform/files/ssl-tls-mtls-policy/azure-storage-use-secure-tls-policy.tf", - 1, ["sec_ssl_tls_policy"], [2] + 1, + ["sec_ssl_tls_policy"], + [2], ) self.__help_test( "tests/security/terraform/files/ssl-tls-mtls-policy/cloudfront-secure-tls-policy.tf", - 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 13] + 2, + ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], + [1, 13], ) self.__help_test( "tests/security/terraform/files/ssl-tls-mtls-policy/database-enable.ssl-eforcement.tf", - 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 8] + 2, + ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], + [1, 8], ) self.__help_test( "tests/security/terraform/files/ssl-tls-mtls-policy/database-secure-tls-policy.tf", - 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [2, 22] + 2, + ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], + [2, 22], ) self.__help_test( "tests/security/terraform/files/ssl-tls-mtls-policy/elastic-search-secure-tls-policy.tf", - 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 20] + 2, + ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], + [1, 20], ) self.__help_test( "tests/security/terraform/files/ssl-tls-mtls-policy/elb-secure-tls-policy.tf", - 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 6] + 2, + ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/ssl-tls-mtls-policy/google-compute-secure-tls-policy.tf", - 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 5] + 2, + ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], + [1, 5], ) self.__help_test( "tests/security/terraform/files/ssl-tls-mtls-policy/sql-encrypt-in-transit-data.tf", - 2, ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], [1, 45] + 2, + ["sec_ssl_tls_policy", "sec_ssl_tls_policy"], + [1, 45], ) def test_terraform_use_of_dns_without_dnssec(self): self.__help_test( "tests/security/terraform/files/use-of-dns-without-dnssec/cloud-dns-without-dnssec.tf", - 2, ["sec_dnssec", "sec_dnssec"], [1, 6] + 2, + ["sec_dnssec", "sec_dnssec"], + [1, 6], ) def test_terraform_firewall_misconfiguration(self): self.__help_test( "tests/security/terraform/files/firewall-misconfiguration/alb-drop-invalid-headers.tf", - 2, ["sec_firewall_misconfig", "sec_firewall_misconfig"], [1, 7] + 2, + ["sec_firewall_misconfig", "sec_firewall_misconfig"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/firewall-misconfiguration/alb-exposed-to-internet.tf", - 2, ["sec_firewall_misconfig", "sec_firewall_misconfig"], [1, 7] + 2, + ["sec_firewall_misconfig", "sec_firewall_misconfig"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/firewall-misconfiguration/azure-keyvault-specify-network-acl.tf", - 3, ["sec_firewall_misconfig", "sec_firewall_misconfig", "sec_firewall_misconfig"], [1, 1, 13] + 3, + [ + "sec_firewall_misconfig", + "sec_firewall_misconfig", + "sec_firewall_misconfig", + ], + [1, 1, 13], ) self.__help_test( "tests/security/terraform/files/firewall-misconfiguration/cloudfront-use-waf.tf", - 2, ["sec_firewall_misconfig", "sec_firewall_misconfig"], [1, 14] + 2, + ["sec_firewall_misconfig", "sec_firewall_misconfig"], + [1, 14], ) self.__help_test( "tests/security/terraform/files/firewall-misconfiguration/config-master-authorized-networks.tf", - 1, ["sec_firewall_misconfig"], [1] + 1, + ["sec_firewall_misconfig"], + [1], ) self.__help_test( "tests/security/terraform/files/firewall-misconfiguration/google-compute-inbound-rule-traffic.tf", - 1, ["sec_firewall_misconfig"], [1] + 1, + ["sec_firewall_misconfig"], + [1], ) self.__help_test( "tests/security/terraform/files/firewall-misconfiguration/google-compute-no-ip-forward.tf", - 1, ["sec_firewall_misconfig"], [2] + 1, + ["sec_firewall_misconfig"], + [2], ) self.__help_test( "tests/security/terraform/files/firewall-misconfiguration/google-compute-outbound-rule-traffic.tf", - 1, ["sec_firewall_misconfig"], [1] + 1, + ["sec_firewall_misconfig"], + [1], ) self.__help_test( "tests/security/terraform/files/firewall-misconfiguration/openstack-compute-no-public-access.tf", - 3, ["sec_firewall_misconfig", "sec_firewall_misconfig", "sec_firewall_misconfig"], [1, 1, 10] + 3, + [ + "sec_firewall_misconfig", + "sec_firewall_misconfig", + "sec_firewall_misconfig", + ], + [1, 1, 10], ) def test_terraform_missing_threats_detection_and_alerts(self): self.__help_test( "tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-disabled-alerts.tf", - 1, ["sec_threats_detection_alerts"], [2] + 1, + ["sec_threats_detection_alerts"], + [2], ) self.__help_test( "tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-admin.tf", - 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [1, 7] + 2, + ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/missing-threats-detection-and-alerts/azure-database-email-for-alerts.tf", - 1, ["sec_threats_detection_alerts"], [1] + 1, + ["sec_threats_detection_alerts"], + [1], ) self.__help_test( "tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-center-alert-notifications.tf", - 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [5, 6] + 2, + ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], + [5, 6], ) self.__help_test( "tests/security/terraform/files/missing-threats-detection-and-alerts/azure-security-require-contact-phone.tf", - 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [1, 10] + 2, + ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], + [1, 10], ) self.__help_test( "tests/security/terraform/files/missing-threats-detection-and-alerts/github-repo-vulnerability-alerts.tf", - 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [1, 16] + 2, + ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], + [1, 16], ) self.__help_test( "tests/security/terraform/files/missing-threats-detection-and-alerts/aws-ecr-enable-image-scans.tf", - 2, ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], [1, 19] + 2, + ["sec_threats_detection_alerts", "sec_threats_detection_alerts"], + [1, 19], ) def test_terraform_weak_password_key_policy(self): self.__help_test( "tests/security/terraform/files/weak-password-key-policy/aws-iam-no-password-reuse.tf", - 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + 2, + ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], + [1, 11], ) self.__help_test( "tests/security/terraform/files/weak-password-key-policy/aws-iam-require-lowercase-in-passwords.tf", - 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + 2, + ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], + [1, 11], ) self.__help_test( "tests/security/terraform/files/weak-password-key-policy/aws-iam-require-numbers-in-passwords.tf", - 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + 2, + ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], + [1, 11], ) self.__help_test( "tests/security/terraform/files/weak-password-key-policy/aws-iam-require-symbols-in-passwords.tf", - 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + 2, + ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], + [1, 11], ) self.__help_test( "tests/security/terraform/files/weak-password-key-policy/aws-iam-require-uppercase-in-passwords.tf", - 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + 2, + ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], + [1, 11], ) self.__help_test( "tests/security/terraform/files/weak-password-key-policy/aws-iam-set-max-password-age.tf", - 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + 2, + ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], + [1, 11], ) self.__help_test( "tests/security/terraform/files/weak-password-key-policy/aws-iam-set-minimum-password-length.tf", - 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + 2, + ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], + [1, 11], ) self.__help_test( "tests/security/terraform/files/weak-password-key-policy/azure-keyvault-ensure-secret-expiry.tf", - 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + 2, + ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], + [1, 11], ) self.__help_test( "tests/security/terraform/files/weak-password-key-policy/azure-keyvault-no-purge.tf", - 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 11] + 2, + ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], + [1, 11], ) self.__help_test( "tests/security/terraform/files/weak-password-key-policy/azure-keyvault-ensure-key-expiration-date.tf", - 2, ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], [1, 5] + 2, + ["sec_weak_password_key_policy", "sec_weak_password_key_policy"], + [1, 5], ) def test_terraform_integrity_policy(self): self.__help_test( "tests/security/terraform/files/integrity-policy/aws-ecr-immutable-repo.tf", - 2, ["sec_integrity_policy", "sec_integrity_policy"], [1, 13] + 2, + ["sec_integrity_policy", "sec_integrity_policy"], + [1, 13], ) self.__help_test( "tests/security/terraform/files/integrity-policy/google-compute-enable-integrity-monitoring.tf", - 1, ["sec_integrity_policy"], [3] + 1, + ["sec_integrity_policy"], + [3], ) self.__help_test( "tests/security/terraform/files/integrity-policy/google-compute-enable-virtual-tpm.tf", - 1, ["sec_integrity_policy"], [3] + 1, + ["sec_integrity_policy"], + [3], ) def test_terraform_sensitive_action_by_iam(self): self.__help_test( "tests/security/terraform/files/sensitive-action-by-iam/aws-iam-no-policy-wildcards.tf", - 3, ["sec_sensitive_iam_action", "sec_sensitive_iam_action", "sec_sensitive_iam_action"], [7, 8, 20] + 3, + [ + "sec_sensitive_iam_action", + "sec_sensitive_iam_action", + "sec_sensitive_iam_action", + ], + [7, 8, 20], ) def test_terraform_key_management(self): self.__help_test( "tests/security/terraform/files/key-management/aws-cloudtrail-encryption-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 8] + 2, + ["sec_key_management", "sec_key_management"], + [1, 8], ) self.__help_test( "tests/security/terraform/files/key-management/aws-cloudwatch-log-group-customer-key.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 6] + 2, + ["sec_key_management", "sec_key_management"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/key-management/aws-documentdb-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 7] + 2, + ["sec_key_management", "sec_key_management"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/key-management/aws-dynamodb-table-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 10] + 2, + ["sec_key_management", "sec_key_management"], + [1, 10], ) self.__help_test( "tests/security/terraform/files/key-management/aws-ebs-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 7] + 2, + ["sec_key_management", "sec_key_management"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/key-management/aws-ecr-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 18] + 2, + ["sec_key_management", "sec_key_management"], + [1, 18], ) self.__help_test( "tests/security/terraform/files/key-management/aws-kinesis-stream-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 7] + 2, + ["sec_key_management", "sec_key_management"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/key-management/aws-kms-auto-rotate-keys.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 5] + 2, + ["sec_key_management", "sec_key_management"], + [1, 5], ) self.__help_test( "tests/security/terraform/files/key-management/aws-neptune-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 9] + 2, + ["sec_key_management", "sec_key_management"], + [1, 9], ) self.__help_test( "tests/security/terraform/files/key-management/aws-sns-topic-encryption-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 5] + 2, + ["sec_key_management", "sec_key_management"], + [1, 5], ) self.__help_test( "tests/security/terraform/files/key-management/aws-sqs-queue-encryption-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 5] + 2, + ["sec_key_management", "sec_key_management"], + [1, 5], ) self.__help_test( "tests/security/terraform/files/key-management/aws-ssm-secret-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 7] + 2, + ["sec_key_management", "sec_key_management"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/key-management/azure-storage-account-use-cmk.tf", - 1, ["sec_key_management"], [1] + 1, + ["sec_key_management"], + [1], ) self.__help_test( "tests/security/terraform/files/key-management/google-compute-disk-encryption-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 6] + 2, + ["sec_key_management", "sec_key_management"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/key-management/google-compute-no-project-wide-ssh-keys.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 12] + 2, + ["sec_key_management", "sec_key_management"], + [1, 12], ) self.__help_test( "tests/security/terraform/files/key-management/google-compute-vm-disk-encryption-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 12] + 2, + ["sec_key_management", "sec_key_management"], + [1, 12], ) self.__help_test( "tests/security/terraform/files/key-management/google-kms-rotate-kms-keys.tf", - 3, ["sec_key_management", "sec_key_management", "sec_key_management"], [1, 9, 15] + 3, + ["sec_key_management", "sec_key_management", "sec_key_management"], + [1, 9, 15], ) self.__help_test( "tests/security/terraform/files/key-management/rds-cluster-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 7] + 2, + ["sec_key_management", "sec_key_management"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/key-management/rds-instance-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 8] + 2, + ["sec_key_management", "sec_key_management"], + [1, 8], ) self.__help_test( "tests/security/terraform/files/key-management/rds-performance-insights-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 9] + 2, + ["sec_key_management", "sec_key_management"], + [1, 9], ) self.__help_test( "tests/security/terraform/files/key-management/redshift-cluster-use-cmk.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 7] + 2, + ["sec_key_management", "sec_key_management"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/key-management/s3-encryption-customer-key.tf", - 2, ["sec_key_management", "sec_key_management"], [9, 47] + 2, + ["sec_key_management", "sec_key_management"], + [9, 47], ) self.__help_test( "tests/security/terraform/files/key-management/digitalocean-compute-use-ssh-keys.tf", - 1, ["sec_key_management"], [1] + 1, + ["sec_key_management"], + [1], ) self.__help_test( "tests/security/terraform/files/key-management/google-storage-bucket-encryption-customer-key.tf", - 2, ["sec_key_management", "sec_key_management"], [1, 8] + 2, + ["sec_key_management", "sec_key_management"], + [1, 8], ) - + def test_terraform_network_security_rules(self): self.__help_test( "tests/security/terraform/files/network-security-rules/aws-vpc-ec2-use-tcp.tf", - 1, ["sec_network_security_rules"], [2] + 1, + ["sec_network_security_rules"], + [2], ) self.__help_test( "tests/security/terraform/files/network-security-rules/azure-container-configured-network-policy.tf", - 2, ["sec_network_security_rules", "sec_network_security_rules"], [1, 15] + 2, + ["sec_network_security_rules", "sec_network_security_rules"], + [1, 15], ) self.__help_test( "tests/security/terraform/files/network-security-rules/azure-network-disable-rdp-from-internet.tf", - 2, ["sec_network_security_rules", "sec_network_security_rules"], [8, 32] + 2, + ["sec_network_security_rules", "sec_network_security_rules"], + [8, 32], ) self.__help_test( "tests/security/terraform/files/network-security-rules/azure-network-ssh-blocked-from-internet.tf", - 2, ["sec_network_security_rules", "sec_network_security_rules"], [8, 32] + 2, + ["sec_network_security_rules", "sec_network_security_rules"], + [8, 32], ) self.__help_test( "tests/security/terraform/files/network-security-rules/azure-storage-default-action-deny.tf", - 3, ["sec_network_security_rules", "sec_network_security_rules", "sec_network_security_rules"], [1, 8, 21] + 3, + [ + "sec_network_security_rules", + "sec_network_security_rules", + "sec_network_security_rules", + ], + [1, 8, 21], ) self.__help_test( "tests/security/terraform/files/network-security-rules/azure-synapse-virtual-network-enabled.tf", - 2, ["sec_network_security_rules", "sec_network_security_rules"], [1, 5] + 2, + ["sec_network_security_rules", "sec_network_security_rules"], + [1, 5], ) self.__help_test( "tests/security/terraform/files/network-security-rules/google-compute-no-serial-port.tf", - 1, ["sec_network_security_rules"], [4] + 1, + ["sec_network_security_rules"], + [4], ) self.__help_test( "tests/security/terraform/files/network-security-rules/google-gke-enable-ip-aliasing.tf", - 1, ["sec_network_security_rules"], [1] + 1, + ["sec_network_security_rules"], + [1], ) self.__help_test( "tests/security/terraform/files/network-security-rules/google-gke-enable-network-policy.tf", - 2, ["sec_network_security_rules", "sec_network_security_rules"], [1, 19] + 2, + ["sec_network_security_rules", "sec_network_security_rules"], + [1, 19], ) self.__help_test( "tests/security/terraform/files/network-security-rules/google-iam-no-default-network.tf", - 2, ["sec_network_security_rules", "sec_network_security_rules"], [1, 5] + 2, + ["sec_network_security_rules", "sec_network_security_rules"], + [1, 5], ) def test_terraform_permission_of_iam_policies(self): self.__help_test( "tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-folder-level.tf", - 2, ["sec_permission_iam_policies", "sec_permission_iam_policies"], [4, 10] + 2, + ["sec_permission_iam_policies", "sec_permission_iam_policies"], + [4, 10], ) self.__help_test( "tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-organization-level.tf", - 2, ["sec_permission_iam_policies", "sec_permission_iam_policies"], [4, 10] + 2, + ["sec_permission_iam_policies", "sec_permission_iam_policies"], + [4, 10], ) self.__help_test( "tests/security/terraform/files/permission-of-iam-policies/default-service-account-not-used-at-project-level.tf", - 2, ["sec_permission_iam_policies", "sec_permission_iam_policies"], [4, 10] + 2, + ["sec_permission_iam_policies", "sec_permission_iam_policies"], + [4, 10], ) self.__help_test( "tests/security/terraform/files/permission-of-iam-policies/google-iam-no-folder-level-service-account-impersonation.tf", - 1, ["sec_permission_iam_policies"], [3] + 1, + ["sec_permission_iam_policies"], + [3], ) self.__help_test( "tests/security/terraform/files/permission-of-iam-policies/google-iam-no-organization-level-service-account-impersonation.tf", - 1, ["sec_permission_iam_policies"], [3] + 1, + ["sec_permission_iam_policies"], + [3], ) self.__help_test( "tests/security/terraform/files/permission-of-iam-policies/google-iam-no-project-level-service-account-impersonation.tf", - 1, ["sec_permission_iam_policies"], [3] + 1, + ["sec_permission_iam_policies"], + [3], ) self.__help_test( "tests/security/terraform/files/permission-of-iam-policies/google-iam-no-user-granted-permissions.tf", - 2, ["sec_permission_iam_policies", "sec_permission_iam_policies"], [2, 6] + 2, + ["sec_permission_iam_policies", "sec_permission_iam_policies"], + [2, 6], ) self.__help_test( "tests/security/terraform/files/permission-of-iam-policies/iam-policies-attached-only-to-groups-or-roles.tf", - 1, ["sec_permission_iam_policies"], [7] + 1, + ["sec_permission_iam_policies"], + [7], ) def test_terraform_logging(self): self.__help_test( "tests/security/terraform/files/logging/aws-api-gateway-enable-access-logging.tf", - 4, ["sec_logging", "sec_logging", "sec_logging", "sec_logging"], [1, 4, 10, 17] + 4, + ["sec_logging", "sec_logging", "sec_logging", "sec_logging"], + [1, 4, 10, 17], ) self.__help_test( "tests/security/terraform/files/logging/aws-api-gateway-enable-tracing.tf", - 2, ["sec_logging", "sec_logging"], [1, 9] + 2, + ["sec_logging", "sec_logging"], + [1, 9], ) self.__help_test( "tests/security/terraform/files/logging/aws-cloudfront-enable-logging.tf", - 2, ["sec_logging", "sec_logging"], [1, 13] + 2, + ["sec_logging", "sec_logging"], + [1, 13], ) self.__help_test( "tests/security/terraform/files/logging/aws-cloudtrail-enable-log-validation.tf", - 2, ["sec_logging", "sec_logging"], [1, 7] + 2, + ["sec_logging", "sec_logging"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/logging/aws-cloudtrail-ensure-cloudwatch-integration.tf", - 2, ["sec_logging", "sec_logging"], [1, 7] + 2, + ["sec_logging", "sec_logging"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/logging/aws-documentdb-enable-log-export.tf", - 2, ["sec_logging", "sec_logging"], [1, 7] + 2, + ["sec_logging", "sec_logging"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/logging/aws-eks-enable-control-plane-logging.tf", - 2, ["sec_logging", "sec_logging"], [1, 15] + 2, + ["sec_logging", "sec_logging"], + [1, 15], ) self.__help_test( "tests/security/terraform/files/logging/aws-elastic-search-enable-domain-logging.tf", - 3, ["sec_logging", "sec_logging", "sec_logging"], [1, 17, 36] + 3, + ["sec_logging", "sec_logging", "sec_logging"], + [1, 17, 36], ) self.__help_test( "tests/security/terraform/files/logging/aws-lambda-enable-tracing.tf", - 2, ["sec_logging", "sec_logging"], [1, 6] + 2, + ["sec_logging", "sec_logging"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/logging/aws-mq-enable-audit-logging.tf", - 2, ["sec_logging", "sec_logging"], [1, 10] + 2, + ["sec_logging", "sec_logging"], + [1, 10], ) self.__help_test( "tests/security/terraform/files/logging/aws-mq-enable-general-logging.tf", - 2, ["sec_logging", "sec_logging"], [1, 10] + 2, + ["sec_logging", "sec_logging"], + [1, 10], ) self.__help_test( "tests/security/terraform/files/logging/aws-msk-enable-logging.tf", - 5, ["sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging"], [1, 14, 48, 51, 54] + 5, + ["sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging"], + [1, 14, 48, 51, 54], ) self.__help_test( "tests/security/terraform/files/logging/aws-neptune-enable-log-export.tf", - 2, ["sec_logging", "sec_logging"], [1, 7] + 2, + ["sec_logging", "sec_logging"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/logging/aws-rds-enable-performance-insights.tf", - 2, ["sec_logging", "sec_logging"], [1, 7] + 2, + ["sec_logging", "sec_logging"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/logging/aws-s3-enable-bucket-logging.tf", - 1, ["sec_logging"], [1] + 1, + ["sec_logging"], + [1], ) self.__help_test( "tests/security/terraform/files/logging/azure-container-aks-logging-configured.tf", - 2, ["sec_logging", "sec_logging"], [1, 13] + 2, + ["sec_logging", "sec_logging"], + [1, 13], ) self.__help_test( "tests/security/terraform/files/logging/azure-monitor-activity-log-retention-set.tf", - 2, ["sec_logging", "sec_logging"], [1, 8] + 2, + ["sec_logging", "sec_logging"], + [1, 8], ) self.__help_test( "tests/security/terraform/files/logging/azure-monitor-capture-all-activities.tf", - 2, ["sec_logging", "sec_logging"], [1, 8] + 2, + ["sec_logging", "sec_logging"], + [1, 8], ) self.__help_test( "tests/security/terraform/files/logging/azure-mssql-database-enable-audit.tf", - 1, ["sec_logging"], [1] + 1, + ["sec_logging"], + [1], ) self.__help_test( "tests/security/terraform/files/logging/azure-mssql-server-and-database-retention-period-set.tf", - 3, ["sec_logging", "sec_logging", "sec_logging"], [3, 13, 18] + 3, + ["sec_logging", "sec_logging", "sec_logging"], + [3, 13, 18], ) self.__help_test( "tests/security/terraform/files/logging/azure-mssql-server-enable-audit.tf", - 1, ["sec_logging"], [1] + 1, + ["sec_logging"], + [1], ) self.__help_test( "tests/security/terraform/files/logging/azure-network-retention-policy-set.tf", - 2, ["sec_logging", "sec_logging"], [1, 7] + 2, + ["sec_logging", "sec_logging"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/logging/azure-postgres-configuration-enabled-logs.tf", - 3, ["sec_logging", "sec_logging", "sec_logging"], [5, 12, 19] + 3, + ["sec_logging", "sec_logging", "sec_logging"], + [5, 12, 19], ) self.__help_test( "tests/security/terraform/files/logging/azure-storage-queue-services-logging-enabled.tf", - 6, ["sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging"], [1, 1, 1, 15, 16, 17] + 6, + [ + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + ], + [1, 1, 1, 15, 16, 17], ) self.__help_test( "tests/security/terraform/files/logging/ensure-cloudwatch-log-group-specifies-retention-days.tf", - 2, ["sec_logging", "sec_logging"], [1, 6] + 2, + ["sec_logging", "sec_logging"], + [1, 6], ) self.__help_test( "tests/security/terraform/files/logging/google-compute-enable-vpc-flow-logs.tf", - 1, ["sec_logging"], [1] + 1, + ["sec_logging"], + [1], ) self.__help_test( "tests/security/terraform/files/logging/google-gke-enable-stackdriver-logging.tf", - 1, ["sec_logging"], [2] + 1, + ["sec_logging"], + [2], ) self.__help_test( "tests/security/terraform/files/logging/google-gke-enable-stackdriver-monitoring.tf", - 1, ["sec_logging"], [2] + 1, + ["sec_logging"], + [2], ) self.__help_test( "tests/security/terraform/files/logging/google-sql-database-log-flags.tf", - 12, ["sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging" - , "sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging", "sec_logging"], - [1, 1, 1, 1, 1, 36, 40, 44, 48, 52, 56, 60] + 12, + [ + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + "sec_logging", + ], + [1, 1, 1, 1, 1, 36, 40, 44, 48, 52, 56, 60], ) self.__help_test( "tests/security/terraform/files/logging/storage-logging-enabled-for-blob-service-for-read-requests.tf", - 4, ["sec_logging", "sec_logging", "sec_logging", "sec_logging"], [1, 8, 49, 79] + 4, + ["sec_logging", "sec_logging", "sec_logging", "sec_logging"], + [1, 8, 49, 79], ) self.__help_test( "tests/security/terraform/files/logging/aws-ecs-enable-container-insight.tf", - 3, ["sec_logging", "sec_logging", "sec_logging"], [1, 7, 11] + 3, + ["sec_logging", "sec_logging", "sec_logging"], + [1, 7, 11], ) self.__help_test( "tests/security/terraform/files/logging/aws-vpc-flow-logs-enabled.tf", - 1, ["sec_logging"], [11] + 1, + ["sec_logging"], + [11], ) def test_terraform_attached_resource(self): self.__help_test( "tests/security/terraform/files/attached-resource/aws_route53_attached_resource.tf", - 2, ["sec_attached_resource", "sec_attached_resource"], [12, 16] + 2, + ["sec_attached_resource", "sec_attached_resource"], + [12, 16], ) def test_terraform_versioning(self): self.__help_test( "tests/security/terraform/files/versioning/aws-s3-enable-versioning.tf", - 2, ["sec_versioning", "sec_versioning"], [1, 8] + 2, + ["sec_versioning", "sec_versioning"], + [1, 8], ) self.__help_test( "tests/security/terraform/files/versioning/digitalocean-spaces-versioning-enabled.tf", - 2, ["sec_versioning", "sec_versioning"], [1, 7] + 2, + ["sec_versioning", "sec_versioning"], + [1, 7], ) def test_terraform_naming(self): self.__help_test( "tests/security/terraform/files/naming/aws-ec2-description-to-security-group-rule.tf", - 2, ["sec_naming", "sec_naming"], [1, 14] + 2, + ["sec_naming", "sec_naming"], + [1, 14], ) self.__help_test( "tests/security/terraform/files/naming/aws-ec2-description-to-security-group.tf", - 2, ["sec_naming", "sec_naming"], [1, 5] + 2, + ["sec_naming", "sec_naming"], + [1, 5], ) self.__help_test( "tests/security/terraform/files/naming/aws-elasticache-description-for-security-group.tf", - 2, ["sec_naming", "sec_naming"], [1, 7] + 2, + ["sec_naming", "sec_naming"], + [1, 7], ) self.__help_test( "tests/security/terraform/files/naming/naming-rules-storage-accounts.tf", - 2, ["sec_naming", "sec_naming"], [2, 21] + 2, + ["sec_naming", "sec_naming"], + [2, 21], ) self.__help_test( "tests/security/terraform/files/naming/openstack-networking-describe-security-group.tf", - 2, ["sec_naming", "sec_naming"], [1, 5] + 2, + ["sec_naming", "sec_naming"], + [1, 5], ) self.__help_test( "tests/security/terraform/files/naming/google-gke-use-cluster-labels.tf", - 2, ["sec_naming", "sec_naming"], [1, 19] + 2, + ["sec_naming", "sec_naming"], + [1, 19], ) def test_terraform_replication(self): self.__help_test( "tests/security/terraform/files/replication/s3-bucket-cross-region-replication.tf", - 2, ["sec_replication", "sec_replication"], [9, 16] + 2, + ["sec_replication", "sec_replication"], + [9, 16], ) From ed865f69bf4cfea34be0fdca923b014f72efd083 Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Tue, 19 Mar 2024 20:53:46 +0000 Subject: [PATCH 2/3] add black linter to CI --- .github/workflows/lint.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..2fd2aea2 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,21 @@ +name: Lint +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3.5.2 + + - name: Run linter + uses: psf/black@stable + with: + options: "--check --verbose" + version: "23.3.0" \ No newline at end of file From f7a12ed161c6eddeb7f71747526eb5690ffc59b1 Mon Sep 17 00:00:00 2001 From: Nfsaavedra Date: Tue, 19 Mar 2024 20:55:51 +0000 Subject: [PATCH 3/3] run black on scripts folder --- scripts/docker_images_scraper.py | 11 ++++------- scripts/obsolete_commads_scraper.py | 8 ++++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/scripts/docker_images_scraper.py b/scripts/docker_images_scraper.py index 6e7a67a9..4e006df3 100644 --- a/scripts/docker_images_scraper.py +++ b/scripts/docker_images_scraper.py @@ -1,18 +1,15 @@ import requests next_url = "https://hub.docker.com/api/content/v1/products/search?image_filter=official&page=1&page_size=100&q=&type=image" -headers = { - "Accept": "application/json", - "Search-Version": "v3" -} +headers = {"Accept": "application/json", "Search-Version": "v3"} images_list = [] while next_url: res = requests.get(next_url, headers=headers).json() - next_url = res['next'] - images = [i['name'] for i in res['summaries']] + next_url = res["next"] + images = [i["name"] for i in res["summaries"]] images_list += images -with open("official_images", 'w') as f: +with open("official_images", "w") as f: f.write("\n".join(images_list)) diff --git a/scripts/obsolete_commads_scraper.py b/scripts/obsolete_commads_scraper.py index 17341f2a..8fbcb59a 100644 --- a/scripts/obsolete_commads_scraper.py +++ b/scripts/obsolete_commads_scraper.py @@ -5,9 +5,9 @@ page = requests.get(URL) soup = BeautifulSoup(page.content, "html.parser") -table = soup.find('table', cellpadding="5", border="1") -tds = table.find_all('td', valign='top') +table = soup.find("table", cellpadding="5", border="1") +tds = table.find_all("td", valign="top") obsolete = [td.text for td in tds if len(td) == 1] -with open("obsolete_commands", 'w') as f: - f.write("\n".join(obsolete)) \ No newline at end of file +with open("obsolete_commands", "w") as f: + f.write("\n".join(obsolete))