import json import re import traceback from flask import Flask, request, g import search from helper_methods import callback_function, check_if_rejection from autoload import autoload import database from operations import check_rule from cloudwatch_logger import print_log, flush_cloudwatch_logs app = Flask(__name__) sources = None def parse_json(data): """ @Ritika This function begins with setting up all the configurations and data fetches. It then iterates over the policies and rules that are passed in the request json. Based on the outcomes of criteria, it assigns a policy status for each policy. Based on the outcomes of each policy status, it assigns a final status. It returns a json response outlining each policy/rule that has run. """ source_result = against_result = log_details = [] localData = autoload(json_data) rules = {} rules_json = data.get("rules", {}) status = False # Safe fetch with logging if 'rules' is missing at the root if not rules_json: print_log("WARNING: 'rules' key is missing or empty in the root data payload.") is_rejection = check_if_rejection(rules_json) for ruleName, policy in rules_json.items(): policy_status = True failed_condition = None request_payload = policy.get('payload', {}) print_log(request_payload.get("identifier")) g.payload = request_payload # Safely get ruleData and log if missing ruleData = policy.get('rule') if not ruleData: print_log(f"[{ruleName}] WARNING: 'rule' array is missing from policy payload.") ruleData = [] rule_list = [] # Adding Context: Prefix logs with the Policy/ruleName payload_rule_name = request_payload.get('rule_name', 'MISSING_RULE_NAME') print_log(f"[{ruleName}] Initialization - Payload Rule Name: {payload_rule_name} | Total rules to process: {len(ruleData)}") ruleNum = 1 for rule in ruleData: # Safely get the rule key early so we can use it in all logs key = rule.get("key") if not key: key = f"UNKNOWN_KEY_INDEX_{ruleNum}" print_log(f"[{ruleName}] WARNING: 'key' is missing in rule definition at index {ruleNum}.") print_log(f"[{ruleName} | Rule: {key}] --- Starting Evaluation ---") fallback = rule.get("fallback") if fallback is True: print_log(f"[{ruleName} | Rule: {key}] Skipped: Marked explicitly as a fallback rule.") ruleNum += 1 continue try: print_log(f"[{ruleName} | Rule: {key}] Executing is_rule_valid...") result, source_result, against_result, log_details = is_rule_valid(rule, localData, is_rejection) except Exception as ex: print_log(f"[{ruleName} | Rule: {key}] EXCEPTION during is_rule_valid: {ex}") if fallback: # Note: Assuming 'fallback' can be a dictionary based on your original code fallback_name = fallback.get("rule") if isinstance(fallback, dict) else None if fallback_name: print_log(f"[{ruleName} | Rule: {key}] Attempting fallback execution with rule: {fallback_name}") fallback_rule = get_rule_by_key(ruleData, fallback_name) if fallback_rule: result, source_result, against_result, log_details = is_rule_valid(fallback_rule, localData, is_rejection, True) else: print_log(f"[{ruleName} | Rule: {key}] Fallback rule '{fallback_name}' not found in ruleData.") else: print_log(f"[{ruleName} | Rule: {key}] Fallback configuration missing 'rule' name.") else: print_log(f"[{ruleName} | Rule: {key}] ERROR: Rule failed and no fallback configured. Logging failure and continuing.") # 1. Instead of raising an exception, assign default failure values result = (False, str(ex)) source_result = [] against_result = [] log_details = [f"Execution halted: {str(ex)}"] print_log(f"[{ruleName} | Rule: {key}] Evaluation Result: {result}") rule_list.append({ "key": key, "result": result[0], "reason": result[1], "source": source_result, "against": against_result, "log": log_details }) if result[0] is not True: policy_status = False failed_condition = {'key': key, 'reason': result[1]} print_log(f"[{ruleName} | Rule: {key}] Condition Failed. Reason: {result[1]}") print_log(f"[{ruleName} | Rule: {key}] --- Evaluation Ended ---") ruleNum += 1 rules[ruleName] = { 'policy_status': policy_status, 'rules': rule_list, 'failed_condition': failed_condition if failed_condition else None } if rules[ruleName]['policy_status']: status = True rules['status'] = status return rules def get_rule_by_key(rule_data, key: str): """ @Ritika Loops through rule data to extract a specific rule by its name. :param rule_data: list of dictionaries :param key: str :return: json """ matching_items = [d for d in rule_data if d.get("key") == key] if matching_items: return matching_items[0] else: return None # or handle the case where no match is found def create_function(data): import dynamic_functions lang_functions = data.get("functions", {}).get("lang", [{}])[0] # Use the module's __dict__ directly as the namespace # This ensures functions are added directly to the module namespace = dynamic_functions.__dict__ namespace['__builtins__'] = __builtins__ for func_key, func_code in lang_functions["python"][0].items(): func_name = func_key.replace("()", "") exec(compile(func_code, '', 'exec'), namespace) # Extract the actual function name from the code (in case it differs from key) # Look for "def function_name(" pattern match = re.search(r'def\s+(\w+)\s*\(', func_code) if match: actual_func_name = match.group(1) # Register with both the key name (without parentheses) and actual name if actual_func_name in namespace: func = namespace[actual_func_name] dynamic_functions.register_function(func_name, func) # Also register with actual name if different if actual_func_name != func_name: dynamic_functions.register_function(actual_func_name, func) def is_rule_valid(rule, localData, rejection, fallback=False): source_result = getSourceResult(rule) source_option = getSourceOption(rule) log_details = [] if isinstance(source_result, bytes): source_result = source_result.decode('utf-8') against_result, condition = get_against_result(rule, source_result, localData, fallback) print_log(against_result) against_option = getAgainstOption(rule) message = rule.get('value').get('message') if isinstance(against_result, tuple): log_details = against_result[1] against_result = [against_result[0][0], condition] else: against_result = [against_result, condition] result = [None, f"Failed: [{source_result},{against_result[0]}]"] if source_result is not None: optional_settings = [source_option, against_option] result = check_rule(source_result, against_result, optional_settings, rejection, message) return [result, source_result, against_result, log_details] def get_against_result(rule, source_result, localData, fallback=False): value = rule.get("value", {}) against = value.get("against", [{}]) result = None condition = None for _against in against: type = _against.get("type") details = _against.get("details", {}) condition = details.get("condition") if type == "db": result = getValueFromDB(details, source_result) if type == "text" or type == "list": if type == "text": result = details.get("value").lower() else: result = details.get("value") if type == "s3": try: result = getValueFromS3(details, localData) except Exception as ex: print_log(f"get_against_result - exception - {ex}") if fallback: result = None else: raise Exception("Value not found in s3") if type == "key": payload = g.payload result = payload.get(rule.get("key")) return result, condition def getAgainstOption(rule): """ @Ritika Checks if optional parameter is present in against and returns the value if present """ optional = None value = rule.get("value") against = value.get("against", [{}]) for _against in against: optional = _against.get("optional") if optional is not None: return optional def getSourceOption(rule): """ @Ritika Checks if optional parameter is present in source and returns the value if present """ optional = None value = rule.get("value") sources = value.get("source", [{}]) for source in sources: optional = source.get("optional") if optional is not None: return optional def getSourceResult(rule): """ Parses the rule json to extracts the value either from the keys in request json, db or :param rule: json :return: [string, int, bool, list, json] """ value = rule.get("value", {}) sources = value.get("source", [{}]) for source in sources: ItemType = source.get("type") details = source.get("details", {}) if ItemType == "db": return getValueFromDB(details) if ItemType == "text": return details.get("value") if ItemType == "key": payload = g.payload rule_key = rule.get("key") # Check if the key actually exists in the JSON payload if rule_key in payload: return payload[rule_key] else: raise ValueError(f"Data not found: '{rule_key}' is missing from the JSON payload.") def getValueFromDB(details, source_result = None): dbCondition = details.get("dbCondition") dbSelect = details.get("dbSelect") function = details.get("function") tableColumn = details.get("value").replace("sourceFile.", "") tableColumnSplit = tableColumn.split(".") tableName = tableColumnSplit[1] columnName = tableColumnSplit[2] dbConditionString = "" if dbCondition != None: dbConditionString = ' '.join(dbCondition) if "where" in dbConditionString: dbConditionString = dbConditionString.replace("where", "and") if dbSelect != None and len(dbSelect) > 0: columnName = dbSelect[0].replace("var1", columnName) primary_column = details.get("primaryColumn") if primary_column is not None: payload = g.payload primary_column = " where "+details.get("primaryColumn")+" = "+str(payload.get("identifier")) else: dbConditionString = dbConditionString.replace("and", "where", 1) primary_column = "" # replace db.source value to actual value if "db.source." in dbConditionString: for dbConditionStringItem in dbConditionString.split("="): if "db.source." in dbConditionStringItem: dbConditionString = replaceDbSourceValue(getQuotedValue(dbConditionStringItem), dbConditionString, source_result) dbConditionString = simplifyDBRaw(dbConditionString) query = "select "+columnName+" from "+tableName+primary_column+" "+dbConditionString if "dbRaw" in details: query = simplifyDBRaw(details.get("dbRaw")) conn = database.get_connection() data = database.execute_select_query(conn, query) if data and len(data) > 0: if function and function.get("call"): return callback_function(function, data) return data[0][columnName] else: return False def simplifyDBRaw(sql): pattern = r'sys\.input\.([a-zA-Z_]+)' def repl(match): var_name = match.group(1) payload = g.payload value = str(payload.get(var_name)) return value return re.sub(pattern, repl, sql).replace("{"," ").replace("}"," ") def getValueFromS3(details, localData): data = localData.get(details.get("config_accessors")) details_to_be_fetched = details.get("value", None) result = {} if data: function = details.get("function") if isinstance(details_to_be_fetched, list): # If 'value' is a list, iterate over it for key in details.get("value"): if search.find_key(data, key): # if key exists fetch_result = search.search_key_in_json(data, key) result[key] = fetch_result else: # if key does not exist raise KeyError(key, "not found IN JSON") elif details_to_be_fetched is None: result = search.search_key_in_json(data, details.get("value")) else: if search.find_key(data, details.get("value")): # if key exists result = search.search_key_in_json(data, details.get("value")) function = details.get("function") else: # if key does not exist raise KeyError(details.get("value"), "not found IN JSON") if function and function.get("call"): return callback_function(function, [result]) return result print("Key not found in the JSON.") raise ValueError(f"Data not found: S3 JSON data for '{details_to_be_fetched}' is empty or missing.") def replaceDbSourceValue(dbString, dbConditionString, source_result): return dbConditionString.replace("db.source."+dbString, source_result) def getQuotedValue(text): start_index = text.find("'") if start_index != -1: # Finding the next occurrence of the single quote starting from the index after the first one end_index = text.find("'", start_index + 1) if end_index != -1: # Extracting the text between the single quotes extracted_text = text[start_index:end_index + 1] return extracted_text.replace("'db.source.", "").replace("'", "") else: return "" else: return "" def validate_keys(data): required_keys = { "identifier", "rules", "functions", "source" } missing_keys = required_keys - set(data.keys()) if missing_keys: return f"Missing required keys: {', '.join(missing_keys)}" # Validate nested objects for rule in data.get("rules", []): source_details = rule.get("source", {}).get("source1", [])[0].get("sourceDetails", {}) if not source_details: return "Missing 'sourceDetails' in 'rules'" if "value" not in source_details: return "Missing 'value' in 'sourceDetails' of 'rules'" nested_source = source_details.get("nested", [{}])[0] if nested_source: nested_source_details = nested_source.get("sourceDetails", {}) if not nested_source_details: return "Missing 'sourceDetails' in nested 'rules'" if "value" not in nested_source_details: return "Missing 'value' in nested 'sourceDetails' of 'rules'" return None def load_source(data): return data.get("sources") def load_data(): """ Extracts the grade from the request body json. Contains a dictionary of rules :return: json """ return request.json.get("grade") def init_connect(data): """Initializes connection with the database using the config""" return database.init_connection(data) @app.after_request def after_request(response): flush_cloudwatch_logs() return response @app.route('/', methods=['POST']) def index(): try: create_function(json_data) req_json_data = load_data() conn = init_connect(json_data) database.set_connection(conn) parsed_data = parse_json(req_json_data) return parsed_data except Exception as ex: print_log(traceback.print_exc()) return {} finally: database.close_connection() with open('config.json', 'r') as file: json_data = json.load(file) if json_data: sources = load_source(json_data) if __name__ == '__main__': app.run(host='0.0.0.0', port=8888, debug=False)