diff --git a/src/cfengine_cli/lint.py b/src/cfengine_cli/lint.py index 8c46d66..c397170 100644 --- a/src/cfengine_cli/lint.py +++ b/src/cfengine_cli/lint.py @@ -18,41 +18,12 @@ from cfbs.validate import validate_config from cfbs.cfbs_config import CFBSConfig from cfbs.utils import find - -DEPRECATED_PROMISE_TYPES = ["defaults", "guest_environments"] -ALLOWED_BUNDLE_TYPES = ["agent", "common", "monitor", "server", "edit_line", "edit_xml"] -BUILTIN_PROMISE_TYPES = { - "access", - "build_xpath", - "classes", - "commands", - "databases", - "defaults", - "delete_attribute", - "delete_lines", - "delete_text", - "delete_tree", - "field_edits", - "files", - "guest_environments", - "insert_lines", - "insert_text", - "insert_tree", - "measurements", - "meta", - "methods", - "packages", - "processes", - "replace_patterns", - "reports", - "roles", - "services", - "set_attribute", - "set_text", - "storage", - "users", - "vars", -} +from cfengine_cli.policy_language import ( + DEPRECATED_PROMISE_TYPES, + ALLOWED_BUNDLE_TYPES, + BUILTIN_PROMISE_TYPES, + BUILTIN_FUNCTIONS, +) def lint_cfbs_json(filename) -> int: @@ -129,7 +100,7 @@ def _find_nodes(filename, lines, node): return matches -def _single_node_checks(filename, lines, node, custom_promise_types, strict): +def _single_node_checks(filename, lines, node, user_definition, strict): """Things which can be checked by only looking at one node, not needing to recurse into children.""" line = node.range.start_point[0] + 1 @@ -150,7 +121,12 @@ def _single_node_checks(filename, lines, node, custom_promise_types, strict): ) return 1 if strict and ( - (promise_type not in BUILTIN_PROMISE_TYPES.union(custom_promise_types)) + ( + promise_type + not in BUILTIN_PROMISE_TYPES.union( + user_definition.get("custom_promise_types", set()) + ) + ) ): _highlight_range(node, lines) print( @@ -179,13 +155,25 @@ def _single_node_checks(filename, lines, node, custom_promise_types, strict): f"Error: Bundle type must be one of ({', '.join(ALLOWED_BUNDLE_TYPES)}), not '{_text(node)}' at {filename}:{line}:{column}" ) return 1 - + if node.type == "calling_identifier": + if strict and ( + _text(node) + not in BUILTIN_FUNCTIONS.union( + user_definition.get("all_bundle_names", set()), + user_definition.get("all_body_names", set()), + ) + ): + _highlight_range(node, lines) + print( + f"Error: Call to unknown function / bundle / body '{_text(node)}' at at {filename}:{line}:{column}" + ) + return 1 return 0 -def _walk(filename, lines, node, custom_promise_types=None, strict=True) -> int: - if custom_promise_types is None: - custom_promise_types = set() +def _walk(filename, lines, node, user_definition=None, strict=True) -> int: + if user_definition is None: + user_definition = {} error_nodes = _find_node_type(filename, lines, node, "ERROR") if error_nodes: @@ -201,18 +189,21 @@ def _walk(filename, lines, node, custom_promise_types=None, strict=True) -> int: errors = 0 for node in _find_nodes(filename, lines, node): - errors += _single_node_checks( - filename, lines, node, custom_promise_types, strict - ) + errors += _single_node_checks(filename, lines, node, user_definition, strict) return errors -def _parse_custom_types(filename, lines, root_node): - ret = set() +def _parse_user_definition(filename, lines, root_node): promise_blocks = _find_node_type(filename, lines, root_node, "promise_block_name") - ret.update(_text(x) for x in promise_blocks) - return ret + bundle_blocks = _find_node_type(filename, lines, root_node, "bundle_block_name") + body_blocks = _find_node_type(filename, lines, root_node, "body_block_name") + + return { + "custom_promise_types": {_text(x) for x in promise_blocks}, + "all_bundle_names": {_text(x) for x in bundle_blocks}, + "all_body_names": {_text(x) for x in body_blocks}, + } def _parse_policy_file(filename): @@ -234,7 +225,7 @@ def lint_policy_file( original_line=None, snippet=None, prefix=None, - custom_promise_types=None, + user_definition=None, strict=True, ): assert original_filename is None or type(original_filename) is str @@ -251,8 +242,8 @@ def lint_policy_file( assert os.path.isfile(filename) assert filename.endswith((".cf", ".cfengine3", ".cf3", ".cf.sub")) - if custom_promise_types is None: - custom_promise_types = set() + if user_definition is None: + user_definition = {} tree, lines, original_data = _parse_policy_file(filename) root_node = tree.root_node @@ -284,7 +275,7 @@ def lint_policy_file( else: print(f"Error: Empty policy file '{filename}'") errors += 1 - errors += _walk(filename, lines, root_node, custom_promise_types, strict) + errors += _walk(filename, lines, root_node, user_definition, strict) if prefix: print(prefix, end="") if errors == 0: @@ -323,34 +314,33 @@ def lint_folder(folder, strict=True): else: errors += lint_single_file(filename) - custom_promise_types = set() + user_definition = {} # First pass: Gather custom types for filename in policy_files if strict else []: tree, lines, _ = _parse_policy_file(filename) if tree.root_node.type == "source_file": - custom_promise_types.update( - _parse_custom_types(filename, lines, tree.root_node) - ) + for key, val in _parse_user_definition( + filename, lines, tree.root_node + ).items(): + user_definition[key] = user_definition.get(key, set()).union(val) # Second pass: lint all policy files for filename in policy_files: errors += lint_policy_file( - filename, custom_promise_types=custom_promise_types, strict=strict + filename, user_definition=user_definition, strict=strict ) return errors -def lint_single_file(file, custom_promise_types=None, strict=True): +def lint_single_file(file, user_definition=None, strict=True): assert os.path.isfile(file) if file.endswith("/cfbs.json"): return lint_cfbs_json(file) if file.endswith(".json"): return lint_json(file) assert file.endswith(".cf") - return lint_policy_file( - file, custom_promise_types=custom_promise_types, strict=strict - ) + return lint_policy_file(file, user_definition=user_definition, strict=strict) def lint_single_arg(arg, strict=True): diff --git a/src/cfengine_cli/main.py b/src/cfengine_cli/main.py index 71f358f..adb96e3 100644 --- a/src/cfengine_cli/main.py +++ b/src/cfengine_cli/main.py @@ -56,7 +56,7 @@ def _get_arg_parser(): "--strict", type=str, default="yes", - help="Strict mode. Default=yes, checks for undefined promisetypes", + help="Strict mode. Default=yes, checks for undefined promise types, bundles, bodies, functions", ) lnt.add_argument("files", nargs="*", help="Files to format") subp.add_parser( diff --git a/src/cfengine_cli/policy_language.py b/src/cfengine_cli/policy_language.py new file mode 100644 index 0000000..2dbb8b9 --- /dev/null +++ b/src/cfengine_cli/policy_language.py @@ -0,0 +1,239 @@ +# These constants are temporary and may change in the future +# TODO: Find a way to extract these from the generated "syntax-definition"-json file + +DEPRECATED_PROMISE_TYPES = ["defaults", "guest_environments"] +ALLOWED_BUNDLE_TYPES = ["agent", "common", "monitor", "server", "edit_line", "edit_xml"] +BUILTIN_PROMISE_TYPES = { + "access", + "build_xpath", + "classes", + "commands", + "databases", + "defaults", + "delete_attribute", + "delete_lines", + "delete_text", + "delete_tree", + "field_edits", + "files", + "guest_environments", + "insert_lines", + "insert_text", + "insert_tree", + "measurements", + "meta", + "methods", + "packages", + "processes", + "replace_patterns", + "reports", + "roles", + "services", + "set_attribute", + "set_text", + "storage", + "users", + "vars", +} +BUILTIN_FUNCTIONS = { + "accessedbefore", + "accumulated", + "ago", + "and", + "basename", + "bundlesmatching", + "bundlestate", + "callstack_callers", + "callstack_promisers", + "canonify", + "canonifyuniquely", + "cf_version_after", + "cf_version_at", + "cf_version_before", + "cf_version_between", + "cf_version_maximum", + "cf_version_minimum", + "changedbefore", + "classesmatching", + "classfiltercsv", + "classfilterdata", + "classify", + "classmatch", + "concat", + "countclassesmatching", + "countlinesmatching", + "data_expand", + "data_readstringarray", + "data_readstringarrayidx", + "data_regextract", + "data_sysctlvalues", + "datastate", + "difference", + "dirname", + "diskfree", + "escape", + "eval", + "every", + "execresult", + "execresult_as_data", + "expandrange", + "file_hash", + "fileexists", + "filesexist", + "filesize", + "filestat", + "filter", + "findfiles", + "findfiles_up", + "findlocalgroups", + "findlocalusers", + "findprocesses", + "format", + "getacls", + "getbundlemetatags", + "getclassmetatags", + "getenv", + "getfields", + "getgid", + "getgroupinfo", + "getgroups", + "getindices", + "getuid", + "getuserinfo", + "getusers", + "getvalues", + "getvariablemetatags", + "grep", + "groupexists", + "hash", + "hash_to_int", + "hashmatch", + "host2ip", + "hostinnetgroup", + "hostrange", + "hostsseen", + "hostswithclass", + "hostswithgroup", + "hubknowledge", + "ifelse", + "int", + "intersection", + "ip2host", + "iprange", + "irange", + "is_type", + "isconnectable", + "isdir", + "isexecutable", + "isgreaterthan", + "isipinsubnet", + "islessthan", + "islink", + "isnewerthan", + "isnewerthantime", + "isplain", + "isreadable", + "isvariable", + "join", + "lastnode", + "laterthan", + "ldaparray", + "ldaplist", + "ldapvalue", + "length", + "lsdir", + "makerule", + "maparray", + "mapdata", + "maplist", + "max", + "mean", + "mergedata", + "min", + "network_connections", + "none", + "not", + "now", + "nth", + "on", + "or", + "packagesmatching", + "packageupdatesmatching", + "parseintarray", + "parsejson", + "parserealarray", + "parsestringarray", + "parsestringarrayidx", + "parseyaml", + "peerleader", + "peerleaders", + "peers", + "processexists", + "product", + "randomint", + "read_module_protocol", + "readcsv", + "readdata", + "readenvfile", + "readfile", + "readintarray", + "readintlist", + "readjson", + "readrealarray", + "readreallist", + "readstringarray", + "readstringarrayidx", + "readstringlist", + "readtcp", + "readyaml", + "regarray", + "regcmp", + "regex_replace", + "regextract", + "registryvalue", + "regldap", + "regline", + "reglist", + "remoteclassesmatching", + "remotescalar", + "returnszero", + "reverse", + "rrange", + "search_up", + "selectservers", + "shuffle", + "some", + "sort", + "splayclass", + "splitstring", + "storejson", + "strcmp", + "strftime", + "string", + "string_downcase", + "string_head", + "string_length", + "string_mustache", + "string_replace", + "string_reverse", + "string_split", + "string_tail", + "string_trim", + "string_upcase", + "sublist", + "sum", + "sysctlvalue", + "translatepath", + "type", + "unique", + "url_get", + "usemodule", + "userexists", + "useringroup", + "validdata", + "validjson", + "variablesmatching", + "variablesmatching_as_data", + "variance", + "version_compare", +}