def prepare_to_run_command(self, cmd): self.LOG.debug(f"[*] prepare_to_run_command() '{cmd.cmd_name}'") # # Process ReadTheDocs web browser request here and then # fall through, which also produces help output on the # command line. The webbrowser module doesn't seem to work # consistently on Ubuntu Linux, so this helps a little # though may be confusing when you don't get a browser opening # up like you expect. rtd_url = 'https://python-secrets.readthedocs.io/en/latest/usage.html' if cmd.cmd_name == 'help' and self.options.rtd: for line in [ '[+] Opening online documentation for python_secrets on ReadTheDocs.', # noqa '[+] If a browser does not open, make sure that you are online and/or', # noqa '[+] enter the following URL in your chosen browser:', rtd_url, ]: self.LOG.info(line) print('\n\n') bell() time.sleep(3) # TODO(dittrich): Add more specificity # FYI, new= is ignored on Windows per: # https://stackoverflow.com/questions/1997327/python-webbrowser-open-setting-new-0-to-open-in-the-same-browser-window-does # noqa webbrowser.open(rtd_url, new=0, autoraise=True) self.timer.start() os.umask(self.options.umask) self.LOG.debug(f"[+] using environment '{self.options.environment}'") self.environment = self.options.environment self.secrets_basedir = self.options.secrets_basedir # Don't output error messages when "complete" command used if cmd.cmd_name != 'complete': SecretsEnvironment.permissions_check( self.secrets_basedir, verbose_level=self.options.verbose_level, ) self.secrets_file = self.options.secrets_file self.secrets = SecretsEnvironment( environment=self.environment, secrets_basedir=self.secrets_basedir, secrets_file=self.secrets_file, export_env_vars=self.options.export_env_vars, preserve_existing=self.options.preserve_existing, verbose_level=self.options.verbose_level, env_var_prefix=self.options.env_var_prefix, )
def prepare_to_run_command(self, cmd): self.LOG.debug('prepare_to_run_command "{0}"'.format(cmd.cmd_name)) self.timer.start() os.umask(self.options.umask) self.LOG.debug('using environment "{}"'.format( self.options.environment)) self.environment = self.options.environment self.secrets_basedir = self.options.secrets_basedir # Don't output error messages when "complete" command used if cmd.cmd_name != 'complete': SecretsEnvironment.permissions_check( self.secrets_basedir, verbose_level=self.options.verbose_level, ) self.secrets_file = self.options.secrets_file self.secrets = SecretsEnvironment( environment=self.environment, secrets_basedir=self.secrets_basedir, secrets_file=self.secrets_file, export_env_vars=self.options.export_env_vars, preserve_existing=self.options.preserve_existing, verbose_level=self.options.verbose_level, env_var_prefix=self.options.env_var_prefix, )
def take_action(self, parsed_args): self.LOG.debug('[*] setting secrets') if (len(parsed_args.arg) == 0 and not parsed_args.undefined and not parsed_args.from_options): raise RuntimeError('[-] no secrets specified to be set') se = self.app.secrets se.read_secrets_and_descriptions() options = dict(se.Options) # TODO(dittrich): Use Variable map like this elsewhere variables = dict(se.Variable) types = dict(se.Type) from_env = None if parsed_args.from_environment is not None: from_env = SecretsEnvironment( environment=parsed_args.from_environment) from_env.read_secrets() options = dict(from_env.secrets.Options) variables = dict(from_env.secrets.Variable) types = dict(from_env.secrets.Type) args = (list(variables.keys()) if not len(parsed_args.arg) else parsed_args.arg) if parsed_args.undefined: # Downselect to just those currently undefined args = [k for k, v in variables if v in [None, '']] if not len(args): raise RuntimeError('[-] no secrets identified to be set') for arg in args: k, v, k_type = None, None, None if parsed_args.from_options: k, v, k_type = (arg, options.get(arg, '').split(',')[0], types.get(arg)) # Don't set from options if the type is generable if is_generable(k_type): continue v = None if v == '*' else v elif '=' not in arg: # No value was specified with the argument k = arg k_type = self.app.secrets.get_type(k) if k_type is None: self.LOG.info(f"[-] no description for '{k}'") raise RuntimeError( f"[-] variable '{k}' has no description") if from_env is not None: # Getting value from same var, different environment v = from_env.get_secret(k, allow_none=True) else: # Try to prompt user for value if (k_type == 'boolean' and k not in self.app.secrets.Options): # Default options for boolean type self.app.secrets.Options[k] = BOOLEAN_OPTIONS if k in self.app.secrets.Options: # Attempt to select from list of option dictionaries v = psec.utils.prompt_options_dict( options=self.app.secrets.Options[k], prompt=self.app.secrets.get_prompt(k)) else: # Just ask user for value v = psec.utils.prompt_string( prompt=self.app.secrets.get_prompt(k), default=("" if v is None else v)) else: # ('=' in arg) # Assignment syntax found (a=b) lhs, rhs = arg.split('=') k_type = self.app.secrets.get_type(lhs) if k_type is None: self.LOG.info(f"[-] no description for '{lhs}'") raise RuntimeError( f"[-] variable '{lhs}' has no description") k = lhs if from_env is not None: # Get value from different var in different environment v = from_env.get_secret(rhs, allow_none=True) self.LOG.info(f"[+] getting value from '{rhs}' in " f"environment '{str(from_env)}'") else: # Value was specified in arg v = rhs if v is not None: # Is the value indirectly referenced? if v.startswith('@'): if v[1] == '~': _path = os.path.expanduser(v[1:]) else: _path = v[1:] with open(_path, 'r') as f: v = f.read().strip() elif v.startswith('!'): # >> Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input. # noqa # Severity: Low Confidence: High # Location: psec/secrets.py:641 p = run(v[1:].split(), stdout=PIPE, stderr=PIPE, shell=False) v = p.stdout.decode('UTF-8').strip() # After all that, did we get a value? if v is None: self.LOG.info(f"[-] could not obtain value for '{k}'") else: self.LOG.debug(f"[+] setting variable '{k}'") self.app.secrets.set_secret(k, v)
def take_action(self, parsed_args): self.LOG.debug('[*] returning secrets path') e = SecretsEnvironment(environment=parsed_args.environment) print(e.secrets_file_path())
def build_option_parser(self, description, version): parser = super(PythonSecretsApp, self).build_option_parser(description, version) parser.formatter_class = argparse.RawDescriptionHelpFormatter # Global options parser.add_argument('--elapsed', action='store_true', dest='elapsed', default=False, help='Print elapsed time on exit (default: False)') _env = SecretsEnvironment() parser.add_argument('-d', '--secrets-basedir', metavar='<secrets-basedir>', dest='secrets_basedir', default=_env.secrets_basedir(), help="Root directory for holding secrets " + "(Env: D2_SECRETS_BASEDIR; default: {})".format( _env.secrets_basedir())) default_env = default_environment() parser.add_argument( '-e', '--environment', metavar='<environment>', dest='environment', default=default_env, help="Deployment environment selector " + "(Env: D2_ENVIRONMENT; default: {})".format(default_env)) parser.add_argument('-s', '--secrets-file', metavar='<secrets-file>', dest='secrets_file', default=_env.secrets_basename(), help="Secrets file (default: {})".format( _env.secrets_basename())) parser.add_argument('-P', '--env-var-prefix', metavar='<prefix>', dest='env_var_prefix', default=None, help="Prefix string for environment variables " + "(default: None)") parser.add_argument('-E', '--export-env-vars', action='store_true', dest='export_env_vars', default=False, help="Export secrets as environment variables " + "(default: False)") parser.add_argument( '--preserve-existing', action='store_true', dest='preserve_existing', default=False, help=("Don't allow over-writing existing environment variables " "(default: False)")) parser.add_argument('--init', action='store_true', dest='init', default=False, help="Initialize directory for holding secrets.") parser.add_argument('--umask', metavar='<umask>', type=umask, dest='umask', default=DEFAULT_UMASK, help="Mask to apply during app execution " + "(default: {:#05o})".format(DEFAULT_UMASK)) parser.epilog = textwrap.dedent("""\ For programs that inherit values through environment variables, you can export secrets using the ``-E`` option to the ``run`` subcommand, e.g. ``psec -E run -- terraform plan -out=$(psec environments path --tmpdir)/tfplan`` To improve overall security when doing this, a default process umask of {:#05o} is set when the app initializes. When running programs like the example above where they create sensitive files in the environment directory, this reduces the chance that secrets created during execution will end up with overly broad permissions. If you need to relax these permissions, use the ``--umask`` option to apply the desired mask. Environment Variables: D2_ENVIRONMENT Defaults the environment identifier. D2_SECRETS_BASEDIR Defaults the base directory for storing secrets. D2_SECRETS_BASENAME Defaults the base name for secrets storage files. D2_NO_REDACT Defaults redaction setting for ``secrets show`` command. """.format(DEFAULT_UMASK)) # noqa return parser
class PythonSecretsApp(App): """Python secrets application class.""" def __init__(self): super(PythonSecretsApp, self).__init__( description=__doc__.strip(), version=__release__ if __release__ != __version__ else __version__, command_manager=CommandManager(namespace='psec'), deferred_help=True, ) self.secrets = None self.environment = None self.secrets_basedir = None self.secrets_file = None self.timer = Timer() def build_option_parser(self, description, version): parser = super(PythonSecretsApp, self).build_option_parser(description, version) parser.formatter_class = argparse.RawDescriptionHelpFormatter # Global options parser.add_argument('--elapsed', action='store_true', dest='elapsed', default=False, help='Print elapsed time on exit (default: False)') _env = SecretsEnvironment() parser.add_argument('-d', '--secrets-basedir', metavar='<secrets-basedir>', dest='secrets_basedir', default=_env.secrets_basedir(), help="Root directory for holding secrets " + "(Env: D2_SECRETS_BASEDIR; default: {})".format( _env.secrets_basedir())) default_env = default_environment() parser.add_argument( '-e', '--environment', metavar='<environment>', dest='environment', default=default_env, help="Deployment environment selector " + "(Env: D2_ENVIRONMENT; default: {})".format(default_env)) parser.add_argument('-s', '--secrets-file', metavar='<secrets-file>', dest='secrets_file', default=_env.secrets_basename(), help="Secrets file (default: {})".format( _env.secrets_basename())) parser.add_argument('-P', '--env-var-prefix', metavar='<prefix>', dest='env_var_prefix', default=None, help="Prefix string for environment variables " + "(default: None)") parser.add_argument('-E', '--export-env-vars', action='store_true', dest='export_env_vars', default=False, help="Export secrets as environment variables " + "(default: False)") parser.add_argument( '--preserve-existing', action='store_true', dest='preserve_existing', default=False, help=("Don't allow over-writing existing environment variables " "(default: False)")) parser.add_argument('--init', action='store_true', dest='init', default=False, help="Initialize directory for holding secrets.") parser.add_argument('--umask', metavar='<umask>', type=umask, dest='umask', default=DEFAULT_UMASK, help="Mask to apply during app execution " + "(default: {:#05o})".format(DEFAULT_UMASK)) parser.epilog = textwrap.dedent("""\ For programs that inherit values through environment variables, you can export secrets using the ``-E`` option to the ``run`` subcommand, e.g. ``psec -E run -- terraform plan -out=$(psec environments path --tmpdir)/tfplan`` To improve overall security when doing this, a default process umask of {:#05o} is set when the app initializes. When running programs like the example above where they create sensitive files in the environment directory, this reduces the chance that secrets created during execution will end up with overly broad permissions. If you need to relax these permissions, use the ``--umask`` option to apply the desired mask. Environment Variables: D2_ENVIRONMENT Defaults the environment identifier. D2_SECRETS_BASEDIR Defaults the base directory for storing secrets. D2_SECRETS_BASENAME Defaults the base name for secrets storage files. D2_NO_REDACT Defaults redaction setting for ``secrets show`` command. """.format(DEFAULT_UMASK)) # noqa return parser def initialize_app(self, argv): self.LOG.debug('initialize_app') if sys.version_info <= (3, 6): raise RuntimeError('This program uses the Python "secrets" ' + 'module, which requires Python 3.6 or higher') def prepare_to_run_command(self, cmd): self.LOG.debug('prepare_to_run_command "{0}"'.format(cmd.cmd_name)) self.timer.start() os.umask(self.options.umask) self.LOG.debug('using environment "{}"'.format( self.options.environment)) self.environment = self.options.environment self.secrets_basedir = self.options.secrets_basedir # Don't output error messages when "complete" command used if cmd.cmd_name != 'complete': SecretsEnvironment.permissions_check( self.secrets_basedir, verbose_level=self.options.verbose_level, ) self.secrets_file = self.options.secrets_file self.secrets = SecretsEnvironment( environment=self.environment, secrets_basedir=self.secrets_basedir, secrets_file=self.secrets_file, export_env_vars=self.options.export_env_vars, preserve_existing=self.options.preserve_existing, verbose_level=self.options.verbose_level, env_var_prefix=self.options.env_var_prefix, ) def clean_up(self, cmd, result, err): self.LOG.debug('clean_up command "{0}"'.format(cmd.cmd_name)) if err: self.LOG.debug('got an error: %s', err) if self.secrets.changed(): self.LOG.info('not writing secrets out due to error') elif cmd.cmd_name != 'complete': if self.secrets.changed(): self.secrets.write_secrets() if (self.options.elapsed or (self.options.verbose_level > 1 and cmd.cmd_name != "complete")): self.timer.stop() elapsed = self.timer.elapsed() self.stderr.write('[+] Elapsed time {}\n'.format(elapsed)) if sys.stderr.isatty(): sys.stderr.write('\a') sys.stderr.flush()
def build_option_parser(self, description, version): parser = super(PythonSecretsApp, self).build_option_parser( description, version ) # OCD hack: Make ``help`` output report main program name, # even if run as ``python -m psec.main`` or such. if parser.prog.endswith('.py'): parser.prog = self.command_manager.namespace parser.formatter_class = argparse.RawDescriptionHelpFormatter # Global options parser.add_argument( '--elapsed', action='store_true', dest='elapsed', default=False, help='Print elapsed time on exit (default: False)' ) _env = SecretsEnvironment() parser.add_argument( '-d', '--secrets-basedir', metavar='<secrets-basedir>', dest='secrets_basedir', default=_env.secrets_basedir(), help="Root directory for holding secrets " + "(Env: D2_SECRETS_BASEDIR; default: {})".format( _env.secrets_basedir()) ) default_env = default_environment() parser.add_argument( '-e', '--environment', metavar='<environment>', dest='environment', default=default_env, help="Deployment environment selector " + "(Env: D2_ENVIRONMENT; default: {})".format( default_env ) ) parser.add_argument( '-s', '--secrets-file', metavar='<secrets-file>', dest='secrets_file', default=_env.secrets_basename(), help="Secrets file (default: {})".format( _env.secrets_basename()) ) parser.add_argument( '-P', '--env-var-prefix', metavar='<prefix>', dest='env_var_prefix', default=None, help="Prefix string for environment variables " + "(default: None)" ) parser.add_argument( '-E', '--export-env-vars', action='store_true', dest='export_env_vars', default=False, help="Export secrets as environment variables " + "(default: False)" ) parser.add_argument( '--preserve-existing', action='store_true', dest='preserve_existing', default=False, help=("Don't allow over-writing existing environment variables " "(default: False)") ) parser.add_argument( '--init', action='store_true', dest='init', default=False, help="Initialize directory for holding secrets." ) parser.add_argument( '--umask', metavar='<umask>', type=umask, dest='umask', default=DEFAULT_UMASK, help="Mask to apply during app execution " + "(default: {:#05o})".format(DEFAULT_UMASK) ) parser.add_argument( '--rtd', action='store_true', dest='rtd', default=False, help=('Open ReadTheDocs documentation on ' '"help" command (default: False)') ) parser.epilog = textwrap.dedent(f"""\ For programs that inherit values through environment variables, you can export secrets using the ``-E`` option to the ``run`` subcommand, e.g. ``psec -E run -- terraform plan -out=$(psec environments path --tmpdir)/tfplan`` The environment variable ``PYTHON_SECRETS_ENVIRONMENT`` will also be exported with the identifier of the associated source environment. To improve overall security when doing this, a default process umask of {DEFAULT_UMASK:#05o} is set when the app initializes. When running programs like the example above where they create sensitive files in the environment directory, this reduces the chance that secrets created during execution will end up with overly broad permissions. If you need to relax these permissions, use the ``--umask`` option to apply the desired mask. To control the browser that is used with the ``help --rtd`` command, set the BROWSER environment variable (e.g., ``BROWSER=lynx``). See: https://github.com/python/cpython/blob/3.8/Lib/webbrowser.py Current working dir: {os.getcwd()} Python interpreter: {sys.executable} (v{sys.version.split()[0]}) Environment variables consumed: BROWSER Default browser for use by webbrowser.open().{show_current_value('BROWSER')} D2_ENVIRONMENT Default environment identifier.{show_current_value('D2_ENVIRONMENT')} D2_SECRETS_BASEDIR Default base directory for storing secrets.{show_current_value('D2_SECRETS_BASEDIR')} D2_SECRETS_BASENAME Default base name for secrets storage files.{show_current_value('D2_SECRETS_BASENAME')} D2_NO_REDACT Default redaction setting for ``secrets show`` command.{show_current_value('D2_NO_REDACT')} """) # noqa return parser
class PythonSecretsApp(App): """Python secrets application class.""" def __init__(self): super().__init__( description=__doc__.strip(), version=__release__ if __release__ != __version__ else __version__, command_manager=CommandManager( namespace='psec' ), deferred_help=True, ) self.secrets = None self.environment = None self.secrets_basedir = None self.secrets_file = None self.timer = Timer() def build_option_parser(self, description, version): parser = super(PythonSecretsApp, self).build_option_parser( description, version ) # OCD hack: Make ``help`` output report main program name, # even if run as ``python -m psec.main`` or such. if parser.prog.endswith('.py'): parser.prog = self.command_manager.namespace parser.formatter_class = argparse.RawDescriptionHelpFormatter # Global options parser.add_argument( '--elapsed', action='store_true', dest='elapsed', default=False, help='Print elapsed time on exit (default: False)' ) _env = SecretsEnvironment() parser.add_argument( '-d', '--secrets-basedir', metavar='<secrets-basedir>', dest='secrets_basedir', default=_env.secrets_basedir(), help="Root directory for holding secrets " + "(Env: D2_SECRETS_BASEDIR; default: {})".format( _env.secrets_basedir()) ) default_env = default_environment() parser.add_argument( '-e', '--environment', metavar='<environment>', dest='environment', default=default_env, help="Deployment environment selector " + "(Env: D2_ENVIRONMENT; default: {})".format( default_env ) ) parser.add_argument( '-s', '--secrets-file', metavar='<secrets-file>', dest='secrets_file', default=_env.secrets_basename(), help="Secrets file (default: {})".format( _env.secrets_basename()) ) parser.add_argument( '-P', '--env-var-prefix', metavar='<prefix>', dest='env_var_prefix', default=None, help="Prefix string for environment variables " + "(default: None)" ) parser.add_argument( '-E', '--export-env-vars', action='store_true', dest='export_env_vars', default=False, help="Export secrets as environment variables " + "(default: False)" ) parser.add_argument( '--preserve-existing', action='store_true', dest='preserve_existing', default=False, help=("Don't allow over-writing existing environment variables " "(default: False)") ) parser.add_argument( '--init', action='store_true', dest='init', default=False, help="Initialize directory for holding secrets." ) parser.add_argument( '--umask', metavar='<umask>', type=umask, dest='umask', default=DEFAULT_UMASK, help="Mask to apply during app execution " + "(default: {:#05o})".format(DEFAULT_UMASK) ) parser.add_argument( '--rtd', action='store_true', dest='rtd', default=False, help=('Open ReadTheDocs documentation on ' '"help" command (default: False)') ) parser.epilog = textwrap.dedent(f"""\ For programs that inherit values through environment variables, you can export secrets using the ``-E`` option to the ``run`` subcommand, e.g. ``psec -E run -- terraform plan -out=$(psec environments path --tmpdir)/tfplan`` The environment variable ``PYTHON_SECRETS_ENVIRONMENT`` will also be exported with the identifier of the associated source environment. To improve overall security when doing this, a default process umask of {DEFAULT_UMASK:#05o} is set when the app initializes. When running programs like the example above where they create sensitive files in the environment directory, this reduces the chance that secrets created during execution will end up with overly broad permissions. If you need to relax these permissions, use the ``--umask`` option to apply the desired mask. To control the browser that is used with the ``help --rtd`` command, set the BROWSER environment variable (e.g., ``BROWSER=lynx``). See: https://github.com/python/cpython/blob/3.8/Lib/webbrowser.py Current working dir: {os.getcwd()} Python interpreter: {sys.executable} (v{sys.version.split()[0]}) Environment variables consumed: BROWSER Default browser for use by webbrowser.open().{show_current_value('BROWSER')} D2_ENVIRONMENT Default environment identifier.{show_current_value('D2_ENVIRONMENT')} D2_SECRETS_BASEDIR Default base directory for storing secrets.{show_current_value('D2_SECRETS_BASEDIR')} D2_SECRETS_BASENAME Default base name for secrets storage files.{show_current_value('D2_SECRETS_BASENAME')} D2_NO_REDACT Default redaction setting for ``secrets show`` command.{show_current_value('D2_NO_REDACT')} """) # noqa return parser def initialize_app(self, argv): self.LOG.debug('[*] initialize_app') if sys.version_info <= (3, 6): raise RuntimeError('This program uses the Python "secrets" ' + 'module, which requires Python 3.6 or higher') def prepare_to_run_command(self, cmd): self.LOG.debug(f"[*] prepare_to_run_command() '{cmd.cmd_name}'") # # Process ReadTheDocs web browser request here and then # fall through, which also produces help output on the # command line. The webbrowser module doesn't seem to work # consistently on Ubuntu Linux, so this helps a little # though may be confusing when you don't get a browser opening # up like you expect. rtd_url = 'https://python-secrets.readthedocs.io/en/latest/usage.html' if cmd.cmd_name == 'help' and self.options.rtd: for line in [ '[+] Opening online documentation for python_secrets on ReadTheDocs.', # noqa '[+] If a browser does not open, make sure that you are online and/or', # noqa '[+] enter the following URL in your chosen browser:', rtd_url, ]: self.LOG.info(line) print('\n\n') bell() time.sleep(3) # TODO(dittrich): Add more specificity # FYI, new= is ignored on Windows per: # https://stackoverflow.com/questions/1997327/python-webbrowser-open-setting-new-0-to-open-in-the-same-browser-window-does # noqa webbrowser.open(rtd_url, new=0, autoraise=True) self.timer.start() os.umask(self.options.umask) self.LOG.debug(f"[+] using environment '{self.options.environment}'") self.environment = self.options.environment self.secrets_basedir = self.options.secrets_basedir # Don't output error messages when "complete" command used if cmd.cmd_name != 'complete': SecretsEnvironment.permissions_check( self.secrets_basedir, verbose_level=self.options.verbose_level, ) self.secrets_file = self.options.secrets_file self.secrets = SecretsEnvironment( environment=self.environment, secrets_basedir=self.secrets_basedir, secrets_file=self.secrets_file, export_env_vars=self.options.export_env_vars, preserve_existing=self.options.preserve_existing, verbose_level=self.options.verbose_level, env_var_prefix=self.options.env_var_prefix, ) def clean_up(self, cmd, result, err): self.LOG.debug(f"[-] clean_up command '{cmd.cmd_name}'") if err: self.LOG.debug(f"[-] got an error: {str(err)}") if self.secrets is not None and self.secrets.changed(): self.LOG.info('[-] not writing secrets out due to error') elif cmd.cmd_name not in ['help', 'complete']: if self.secrets.changed(): self.secrets.write_secrets() if (self.options.elapsed or (self.options.verbose_level > 1 and cmd.cmd_name != "complete")): self.timer.stop() elapsed = self.timer.elapsed() self.stderr.write('[+] elapsed time {}\n'.format(elapsed)) bell()
i for i in dir(object) if not i.startswith('_') and not i.startswith('translate_') ] @contextfunction def include_raw(ctx, name): env = ctx.environment return Markup(env.loader.get_source(env, name)[0]).strip log = logging.getLogger(__name__) LoggingUndefined = make_logging_undefined(logger=log, base=Undefined) # Use the default environment env = SecretsEnvironment() env.read_secrets() aws_private_keypath = env.get_secret('aws_privatekey_path') with open(aws_private_keypath + '.pub', 'r') as f: aws_publickey = f.read().strip() instance_type = env.get_secret('aws_instance_type') pulumi.info(msg="instance_type={}".format(instance_type)) try: ami = env.get_secret('aws_ami_id') except RuntimeError: ami = '' if ami == '': ami = get_linux_ami(instance_type) pulumi.info(msg="ami={}".format(ami))