diff --git a/.gitignore b/.gitignore index 6e20ebee..5e4d057c 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,11 @@ configdata/mysubscription.json log/* realm/* + +# >>> gadriel-code-security >>> +.security/state/ +.security/audit/ +.security/metrics/ +.security/logs/ +.security/cache/ +# <<< gadriel-code-security <<< diff --git a/src/processor/connector/vault.py b/src/processor/connector/vault.py index e366fa75..f028647d 100644 --- a/src/processor/connector/vault.py +++ b/src/processor/connector/vault.py @@ -1,186 +1,252 @@ -""" - Common file for vault functionality. -""" -from builtins import input -import os -from subprocess import Popen, PIPE -from processor.logging.log_handler import getlogger -from processor.helper.config.rundata_utils import get_from_currentdata,\ - put_in_currentdata, add_to_exclude_list -from processor.helper.config.config_utils import config_value -from processor.helper.httpapi.restapi_azure import get_vault_access_token, get_uami_vault_access_token,\ - get_keyvault_secret, set_keyvault_secret, get_all_secrets, delete_keyvault_secret, set_keyvault_secret_with_response - -logger = getlogger() - - -def get_vault_data(secret_key=None): - """Read vault data from config""" - vaulttype = config_value('VAULT', 'type') - val = None - if vaulttype: - if vaulttype == 'azure': - val = get_azure_vault_data(secret_key) - elif vaulttype == 'cyberark': - val = get_cyberark_data(secret_key) - return val - - -def set_vault_data(key_name=None, value=None): - """Update vault data""" - vaulttype = config_value('VAULT', 'type') - val = None - if vaulttype: - if vaulttype == 'azure': - val = set_azure_vault_data(key_name, value) - return val - -def set_vault_data_with_response(key_name=None, value=None): - """Update vault data""" - vaulttype = config_value('VAULT', 'type') - status, response = None, None - if vaulttype: - if vaulttype == 'azure': - status, response = set_azure_vault_data_with_response(key_name, value) - return status, response - -def delete_vault_data(secret_key=None): - """Delete vault data from config""" - vaulttype = config_value('VAULT', 'type') - val = None - if vaulttype: - if vaulttype == 'azure': - val = delete_azure_vault_data(secret_key) - return val - - -def get_all_vault_secrets(): - """Read all vault secrets""" - vaulttype = config_value('VAULT', 'type') - val = None - if vaulttype: - if vaulttype == 'azure': - val = get_all_azure_secrets() - return val - - -def get_all_azure_secrets(): - val = None - vaulttoken = _get_vault_token() - # logger.debug('Vault Token: %s', vaulttoken) - if vaulttoken: - keyvault = config_value('VAULT', 'keyvault') - logger.info('Keyvault: %s', keyvault) - data = get_all_secrets(keyvault, vaulttoken) - if data: - return data - return [] - - -def get_config_value(section, key, env_var, prompt_str=None): - """ Return the client secret used for the current run""" - client_secret = config_value(section, key) - if not client_secret and env_var: - client_secret = os.getenv(env_var, None) - if not client_secret and prompt_str: - key_str = '%s_%s' % (section, key) - client_secret = get_from_currentdata(key_str) - if not client_secret: - client_secret = input(prompt_str) - if client_secret: - put_in_currentdata(key_str, client_secret) - logger.info('Key:%s, sec:%s', key_str, '*' * len(client_secret)) - add_to_exclude_list(key_str) - return client_secret - - -def _get_vault_token(): - """Fetches the bearer token for Azure Vault API calls""" - if 'UAMI' in os.environ and os.environ['UAMI'] == 'true': - vaulttoken = get_uami_vault_access_token() - else: - client_id = config_value('VAULT', 'client_id') - client_secret = get_config_value('VAULT', 'client_secret', 'CLIENTKEY', - 'Enter the client secret to access keyvault: ') - # client_secret = config_value('VAULT', 'client_secret') - tenant_id = config_value('VAULT', 'tenant_id') - # logger.info('Id: %s, secret: %s, tenant: %s', client_id, client_secret, tenant_id) - vaulttoken = get_vault_access_token(tenant_id, client_id, client_secret) - return vaulttoken - - -def get_azure_vault_data(secret_key=None): - """Fetches the bearer token for Azure Vault API calls""" - val = None - vaulttoken = _get_vault_token() - # logger.debug('Vault Token: %s', vaulttoken) - if vaulttoken and secret_key: - keyvault = config_value('VAULT', 'keyvault') - # secret_key = config_value('VAULT', 'secret_key') - logger.info('Keyvault: %s, key:%s', keyvault, '*' * len(secret_key)) - secret_data = get_keyvault_secret(keyvault, secret_key, vaulttoken) - if secret_data and 'value' in secret_data: - val = secret_data['value'] - return val - - -def set_azure_vault_data(secret_key=None, value=None): - """Fetches the bearer token for Azure Vault API calls""" - val = None - vaulttoken = _get_vault_token() - # logger.debug('Vault Token: %s', vaulttoken) - if vaulttoken and secret_key and value: - keyvault = config_value('VAULT', 'keyvault') - # secret_key = config_value('VAULT', 'secret_key') - logger.info('Keyvault: %s, key:%s', keyvault, '*' * len(secret_key)) - sucess = set_keyvault_secret(keyvault, vaulttoken, secret_key, value) - if sucess: - return True - return False - -def set_azure_vault_data_with_response(secret_key=None, value=None): - """Fetches the bearer token for Azure Vault API calls""" - status, data = None, None - vaulttoken = _get_vault_token() - # logger.debug('Vault Token: %s', vaulttoken) - if vaulttoken and secret_key and value: - keyvault = config_value('VAULT', 'keyvault') - logger.info('Keyvault: %s, key:%s', keyvault, '*' * len(secret_key)) - status, data = set_keyvault_secret_with_response(keyvault, vaulttoken, secret_key, value) - return status, data - -def delete_azure_vault_data(secret_key=None): - """"Delete a key from vault""" - success = None - vaulttoken = _get_vault_token() - # logger.debug('Vault Token: %s', vaulttoken) - if vaulttoken and secret_key: - keyvault = config_value('VAULT', 'keyvault') - logger.info('Keyvault: %s, key:%s', keyvault, '*' * len(secret_key)) - success = delete_keyvault_secret(keyvault, secret_key, vaulttoken) - logger.info('Secret Deleted: %s', success) - return success - - -def get_cyberark_data(secret_key=None): - """Get secret value for the secret key""" - val = None - ca_object = config_value('VAULT', 'CA_OBJECT') - ca_safe = config_value('VAULT', 'CA_SAFE') - ca_exe = config_value('VAULT', 'CA_EXE') - ca_appid = config_value('VAULT', 'CA_APPID') - if ca_object and ca_exe and ca_appid: - cmd_args = [ca_exe, 'GetPassword', '-p', 'AppDescs.AppID=%s' % ca_appid, - '-p', 'Query=Safe=%s;Folder=Root;Object=%s-%s' % (ca_safe, ca_object, secret_key), - '-o', 'Password'] - my_process = Popen(cmd_args, stdout=PIPE, - stderr=PIPE, - stdin=PIPE) - out, err = my_process.communicate() - err_result = err.rstrip() if err else None - val = out.decode() if isinstance(out, bytes) else out.rstrip() - if err_result: - val = None - else: - logger.info('Secret Value: %s', '*' * len(val)) - return val +""" + Common file for vault functionality. +""" +from builtins import input +import os +from subprocess import Popen, PIPE +from processor.logging.log_handler import getlogger +from processor.helper.config.rundata_utils import get_from_currentdata,\ + put_in_currentdata, add_to_exclude_list +from processor.helper.config.config_utils import config_value +from processor.helper.httpapi.restapi_azure import get_vault_access_token, get_uami_vault_access_token,\ + get_keyvault_secret, set_keyvault_secret, get_all_secrets, delete_keyvault_secret, set_keyvault_secret_with_response +from processor.helper.httpapi.restapi_aws import get_aws_secret, set_aws_secret, set_aws_secret_with_response,\ + delete_aws_secret, get_all_aws_secrets + +logger = getlogger() + + +def get_vault_data(secret_key=None): + """Read vault data from config""" + vaulttype = config_value('VAULT', 'type') + val = None + if vaulttype: + if vaulttype == 'azure': + val = get_azure_vault_data(secret_key) + elif vaulttype == 'aws': + val = get_aws_vault_data(secret_key) + elif vaulttype == 'cyberark': + val = get_cyberark_data(secret_key) + return val + + +def set_vault_data(key_name=None, value=None): + """Update vault data""" + vaulttype = config_value('VAULT', 'type') + val = None + if vaulttype: + if vaulttype == 'azure': + val = set_azure_vault_data(key_name, value) + elif vaulttype == 'aws': + val = set_aws_vault_data(key_name, value) + return val + +def set_vault_data_with_response(key_name=None, value=None): + """Update vault data""" + vaulttype = config_value('VAULT', 'type') + status, response = None, None + if vaulttype: + if vaulttype == 'azure': + status, response = set_azure_vault_data_with_response(key_name, value) + elif vaulttype == 'aws': + status, response = set_aws_vault_data_with_response(key_name, value) + return status, response + +def delete_vault_data(secret_key=None): + """Delete vault data from config""" + vaulttype = config_value('VAULT', 'type') + val = None + if vaulttype: + if vaulttype == 'azure': + val = delete_azure_vault_data(secret_key) + elif vaulttype == 'aws': + val = delete_aws_vault_data(secret_key) + return val + + +def get_all_vault_secrets(): + """Read all vault secrets""" + vaulttype = config_value('VAULT', 'type') + val = None + if vaulttype: + if vaulttype == 'azure': + val = get_all_azure_secrets() + elif vaulttype == 'aws': + val = get_all_aws_vault_secrets() + return val + + +# AWS Secrets Manager dispatch helpers. Selected when [VAULT] type = aws. +# Required config: [VAULT] aws_secrets_prefix (e.g. prancer/prod/customer170) +# Optional: [VAULT] aws_region (else AWS_REGION env). boto3 default cred chain. +def _aws_kwargs(): + return { + 'prefix': config_value('VAULT', 'aws_secrets_prefix'), + 'region': config_value('VAULT', 'aws_region'), + } + + +def get_aws_vault_data(secret_key=None): + if not secret_key: + return None + data = get_aws_secret(secret_key, **_aws_kwargs()) + return data['value'] if data and 'value' in data else None + + +def set_aws_vault_data(key_name=None, value=None): + if not key_name or value is None: + return False + return set_aws_secret(key_name, value, **_aws_kwargs()) + + +def set_aws_vault_data_with_response(key_name=None, value=None): + if not key_name or value is None: + return None, None + return set_aws_secret_with_response(key_name, value, **_aws_kwargs()) + + +def delete_aws_vault_data(secret_key=None): + if not secret_key: + return False + return delete_aws_secret(secret_key, **_aws_kwargs()) + + +def get_all_aws_vault_secrets(): + return get_all_aws_secrets(**_aws_kwargs()) or [] + + +def get_all_azure_secrets(): + val = None + vaulttoken = _get_vault_token() + # logger.debug('Vault Token: %s', vaulttoken) + if vaulttoken: + keyvault = config_value('VAULT', 'keyvault') + logger.info('Keyvault: %s', keyvault) + data = get_all_secrets(keyvault, vaulttoken) + if data: + return data + return [] + + +def get_config_value(section, key, env_var, prompt_str=None): + """ Return the client secret used for the current run""" + client_secret = config_value(section, key) + if not client_secret and env_var: + client_secret = os.getenv(env_var, None) + if not client_secret and prompt_str: + key_str = '%s_%s' % (section, key) + client_secret = get_from_currentdata(key_str) + if not client_secret: + client_secret = input(prompt_str) + if client_secret: + put_in_currentdata(key_str, client_secret) + logger.info('Key:%s, sec:%s', key_str, '*' * len(client_secret)) + add_to_exclude_list(key_str) + return client_secret + + +def _get_vault_token(): + """Fetches the bearer token for Azure Vault API calls""" + if 'UAMI' in os.environ and os.environ['UAMI'] == 'true': + vaulttoken = get_uami_vault_access_token() + else: + client_id = config_value('VAULT', 'client_id') + client_secret = get_config_value('VAULT', 'client_secret', 'CLIENTKEY', + 'Enter the client secret to access keyvault: ') + # client_secret = config_value('VAULT', 'client_secret') + tenant_id = config_value('VAULT', 'tenant_id') + # logger.info('Id: %s, secret: %s, tenant: %s', client_id, client_secret, tenant_id) + vaulttoken = get_vault_access_token(tenant_id, client_id, client_secret) + return vaulttoken + + +def get_azure_vault_data(secret_key=None): + """Fetches the bearer token for Azure Vault API calls""" + val = None + vaulttoken = _get_vault_token() + # logger.debug('Vault Token: %s', vaulttoken) + if vaulttoken and secret_key: + keyvault = config_value('VAULT', 'keyvault') + # secret_key = config_value('VAULT', 'secret_key') + logger.info('Keyvault: %s, key:%s', keyvault, '*' * len(secret_key)) + secret_data = get_keyvault_secret(keyvault, secret_key, vaulttoken) + if secret_data and 'value' in secret_data: + val = secret_data['value'] + return val + + +def set_azure_vault_data(secret_key=None, value=None): + """Fetches the bearer token for Azure Vault API calls""" + val = None + vaulttoken = _get_vault_token() + # logger.debug('Vault Token: %s', vaulttoken) + if vaulttoken and secret_key and value: + keyvault = config_value('VAULT', 'keyvault') + # secret_key = config_value('VAULT', 'secret_key') + logger.info('Keyvault: %s, key:%s', keyvault, '*' * len(secret_key)) + sucess = set_keyvault_secret(keyvault, vaulttoken, secret_key, value) + if sucess: + return True + return False + +def set_azure_vault_data_with_response(secret_key=None, value=None): + """Fetches the bearer token for Azure Vault API calls""" + status, data = None, None + vaulttoken = _get_vault_token() + # logger.debug('Vault Token: %s', vaulttoken) + if vaulttoken and secret_key and value: + keyvault = config_value('VAULT', 'keyvault') + logger.info('Keyvault: %s, key:%s', keyvault, '*' * len(secret_key)) + status, data = set_keyvault_secret_with_response(keyvault, vaulttoken, secret_key, value) + return status, data + +def delete_azure_vault_data(secret_key=None): + """"Delete a key from vault""" + success = None + vaulttoken = _get_vault_token() + # logger.debug('Vault Token: %s', vaulttoken) + if vaulttoken and secret_key: + keyvault = config_value('VAULT', 'keyvault') + logger.info('Keyvault: %s, key:%s', keyvault, '*' * len(secret_key)) + success = delete_keyvault_secret(keyvault, secret_key, vaulttoken) + logger.info('Secret Deleted: %s', success) + return success + + +def get_cyberark_data(secret_key=None): + """Get secret value for the secret key""" + val = None + ca_object = config_value('VAULT', 'CA_OBJECT') + ca_safe = config_value('VAULT', 'CA_SAFE') + ca_exe = config_value('VAULT', 'CA_EXE') + ca_appid = config_value('VAULT', 'CA_APPID') + if ca_object and ca_exe and ca_appid: + cmd_args = [ca_exe, 'GetPassword', '-p', 'AppDescs.AppID=%s' % ca_appid, + '-p', 'Query=Safe=%s;Folder=Root;Object=%s-%s' % (ca_safe, ca_object, secret_key), + '-o', 'Password'] + my_process = Popen(cmd_args, stdout=PIPE, + stderr=PIPE, + stdin=PIPE) + out, err = my_process.communicate() + err_result = err.rstrip() if err else None + val = out.decode() if isinstance(out, bytes) else out.rstrip() + if err_result: + val = None + else: + logger.info('Secret Value: %s', '*' * len(val)) + return val + + +def get_platform_vault_data(secret_key=None): + """Account-level / platform-shared secret. On AWS resolves from the shared + platform prefix (default prancer/prod/platform) so one value serves every + tenant (used when prancer-account is not deployed). Other vault types: the + account vault is already shared, so defer to get_vault_data.""" + if not secret_key: + return None + vaulttype = config_value('VAULT', 'type') + if vaulttype != 'aws': + return get_vault_data(secret_key) + prefix = config_value('VAULT', 'aws_platform_prefix') or 'prancer/prod/platform' + data = get_aws_secret(secret_key, prefix=prefix, region=config_value('VAULT', 'aws_region')) + return data['value'] if data and 'value' in data else None diff --git a/src/processor/helper/httpapi/restapi_aws.py b/src/processor/helper/httpapi/restapi_aws.py new file mode 100644 index 00000000..d4b7baf5 --- /dev/null +++ b/src/processor/helper/httpapi/restapi_aws.py @@ -0,0 +1,130 @@ +""" +AWS Secrets Manager equivalents of restapi_azure.py. + +Parallel implementation — does not replace the Azure path. Selected via +[VAULT] type = aws in the config (see processor/connector/vault.py). + +Auth: relies on the boto3 default credential chain (env vars, IAM role via +EKS Pod Identity / IRSA, instance profile). No explicit credential handling +needed when running in the cluster. + +Secret naming convention: AWS Secrets Manager uses path-like names. The +[VAULT] aws_secrets_prefix config value is prepended to every secret key, +matching the V3 layout: + + prancer/prod/customer170/ + prancer/prod/platform/ +""" +import json +from processor.logging.log_handler import getlogger + +logger = getlogger() + +_boto3 = None +_boto3_client_cache = {} + + +def _client(service='secretsmanager', region=None): + global _boto3 + if _boto3 is None: + try: + import boto3 + except ImportError: + raise RuntimeError("boto3 required for [VAULT] type=aws. pip install boto3") + _boto3 = boto3 + key = (service, region or 'default') + if key not in _boto3_client_cache: + kwargs = {'region_name': region} if region else {} + _boto3_client_cache[key] = _boto3.client(service, **kwargs) + return _boto3_client_cache[key] + + +def _full_name(secret_key, prefix=None): + if not prefix: + return secret_key + if secret_key.startswith(prefix + '/') or secret_key == prefix: + return secret_key + return f"{prefix.rstrip('/')}/{secret_key.lstrip('/')}" + + +def get_aws_secret(secret_key, prefix=None, region=None): + """Returns dict with 'value' key (parity with Azure response shape).""" + name = _full_name(secret_key, prefix) + client = _client(region=region) + try: + resp = client.get_secret_value(SecretId=name) + val = resp.get('SecretString') + if val is None: + binary = resp.get('SecretBinary') + val = binary.decode('utf-8') if binary else None + logger.info('AWS secret read: %s', '*' * len(name)) + return {'value': val} if val is not None else None + except client.exceptions.ResourceNotFoundException: + logger.warning('AWS secret not found: %s', name) + return None + except Exception as e: + logger.error('AWS secret read failed for %s: %s', name, e) + return None + + +def set_aws_secret(secret_key, value, prefix=None, region=None): + name = _full_name(secret_key, prefix) + client = _client(region=region) + try: + try: + client.put_secret_value(SecretId=name, SecretString=value) + except client.exceptions.ResourceNotFoundException: + client.create_secret(Name=name, SecretString=value) + logger.info('AWS secret written: %s', '*' * len(name)) + return True + except Exception as e: + logger.error('AWS secret write failed for %s: %s', name, e) + return False + + +def set_aws_secret_with_response(secret_key, value, prefix=None, region=None): + name = _full_name(secret_key, prefix) + client = _client(region=region) + try: + try: + resp = client.put_secret_value(SecretId=name, SecretString=value) + except client.exceptions.ResourceNotFoundException: + resp = client.create_secret(Name=name, SecretString=value) + return resp.get('ResponseMetadata', {}).get('HTTPStatusCode', 200), resp + except Exception as e: + logger.error('AWS secret write failed for %s: %s', name, e) + return 500, {'error': str(e)} + + +def delete_aws_secret(secret_key, prefix=None, region=None, recovery_window_days=7): + name = _full_name(secret_key, prefix) + client = _client(region=region) + try: + if recovery_window_days == 0: + client.delete_secret(SecretId=name, ForceDeleteWithoutRecovery=True) + else: + client.delete_secret(SecretId=name, RecoveryWindowInDays=recovery_window_days) + return True + except client.exceptions.ResourceNotFoundException: + return True + except Exception as e: + logger.error('AWS secret delete failed for %s: %s', name, e) + return False + + +def get_all_aws_secrets(prefix=None, region=None): + client = _client(region=region) + out = [] + paginator = client.get_paginator('list_secrets') + filters = [{'Key': 'name', 'Values': [prefix + '/']}] if prefix else [] + try: + for page in paginator.paginate(Filters=filters or None): + for entry in page.get('SecretList', []): + full_name = entry['Name'] + short = full_name[len(prefix) + 1:] if prefix and full_name.startswith(prefix + '/') else full_name + val_resp = client.get_secret_value(SecretId=full_name) + out.append({'key': short, 'value': val_resp.get('SecretString')}) + return out + except Exception as e: + logger.error('AWS secret list failed: %s', e) + return []