def test_cache_old_config_no_new_secret(client, isolated_fs): """ GIVEN a cache of last found secrets same as config ignored-matches and config ignored-matches is a list of strings WHEN I run a scan (therefore finding no secret) THEN config matches is unchanged and cache is empty """ c = Commit() c._patch = _MULTIPLE_SECRETS config = Config() config.matches_ignore = [d["match"] for d in FOUND_SECRETS] cache = Cache() cache.last_found_secrets = FOUND_SECRETS with my_vcr.use_cassette("multiple_secrets"): results = c.scan( client=client, cache=cache, matches_ignore=config.matches_ignore, all_policies=True, verbose=False, ) assert results == [] assert config.matches_ignore == [d["match"] for d in FOUND_SECRETS] assert cache.last_found_secrets == []
def test_auth_login_token_default_instance(self, monkeypatch, cli_fs_runner): """ GIVEN a valid API token WHEN the auth login command is called without --instance with method token THEN the authentication is made against the default instance """ config = Config() assert len(config.auth_config.instances) == 0 self.mock_autho_login_request(monkeypatch, 200, self.VALID_TOKEN_PAYLOAD) cmd = ["auth", "login", "--method=token"] token = "mysupertoken" result = cli_fs_runner.invoke(cli, cmd, color=False, input=token + "\n") config = Config() assert result.exit_code == 0, result.output assert len(config.auth_config.instances) == 1 assert config.auth_config.default_instance in config.auth_config.instances assert (config.auth_config.instances[ config.auth_config.default_instance].account.token == token)
def test_ignore_last_found_preserve_previous_config(client, isolated_fs): """ GIVEN a cache containing new secrets AND a config not empty WHEN I run ignore command THEN existing config option are not wiped out """ config = Config() previous_secrets = [ {"name": "", "match": "previous_secret"}, {"name": "", "match": "other_previous_secret"}, ] previous_paths = {"some_path", "some_other_path"} config.matches_ignore = previous_secrets.copy() config.paths_ignore = previous_paths config.exit_zero = True cache = Cache() cache.last_found_secrets = FOUND_SECRETS ignore_last_found(config, cache) matches_ignore = sorted(config.matches_ignore, key=compare_matches_ignore) found_secrets = sorted(FOUND_SECRETS + previous_secrets, key=compare_matches_ignore) assert matches_ignore == found_secrets assert config.paths_ignore == previous_paths assert config.exit_zero is True
def ignore_last_found(config: Config, cache: Cache) -> int: """ Add last found secrets from .cache_ggshield into ignored_matches in the local .gitguardian.yaml config file so that they are ignored on next run Secrets are added as `hash` """ for secret in cache.last_found_secrets: config.add_ignored_match(secret) config.save() return len(cache.last_found_secrets)
def test_auth_login_token_update_existing_config(self, monkeypatch, cli_fs_runner): """ GIVEN some valid API tokens WHEN the auth login command is called with --method=token THEN the instance configuration is created if it doesn't exist, or updated otherwise """ monkeypatch.setattr( "ggshield.core.client.GGClient.get", Mock(return_value=Mock(ok=True, json=lambda: _TOKEN_RESPONSE_PAYLOAD)), ) instance = "https://dashboard.gitguardian.com" cmd = ["auth", "login", "--method=token", f"--instance={instance}"] token = "myfirstsupertoken" result = cli_fs_runner.invoke(cli, cmd, color=False, input=token + "\n") config = Config() assert result.exit_code == 0, result.output assert config.auth_config.instances[instance].account.token == token token = "mysecondsupertoken" result = cli_fs_runner.invoke(cli, cmd, color=False, input=token + "\n") config = Config() assert result.exit_code == 0, result.output assert len(config.auth_config.instances) == 1 assert config.auth_config.instances[instance].account.token == token second_instance_token = "mythirdsupertoken" second_instance = "https://dashboard.other.gitguardian.com" cmd = [ "auth", "login", "--method=token", f"--instance={second_instance}" ] result = cli_fs_runner.invoke(cli, cmd, color=False, input=second_instance_token + "\n") config = Config() assert result.exit_code == 0, result.output assert len(config.auth_config.instances) == 2 assert config.auth_config.instances[instance].account.token == token assert (config.auth_config.instances[second_instance].account.token == second_instance_token)
def test_instance_not_in_auth_config(self): """ GIVEN a config with a current instance not being a valid configured instance WHEN reading config.api_key THEN it raises """ if "GITGUARDIAN_API_KEY" in os.environ: del os.environ["GITGUARDIAN_API_KEY"] config = Config() config.current_instance = "toto" with pytest.raises(UnknownInstanceError, match="Unknown instance: 'toto'"): config.api_key
def test_cache_catches_last_found_secrets(client, isolated_fs): """ GIVEN an empty cache and an empty config matches-ignore section WHEN I run a scan with multiple secrets THEN cache last_found_secrets is updated with these secrets and saved """ c = Commit() c._patch = _MULTIPLE_SECRETS config = Config() setattr(config, "matches_ignore", []) cache = Cache() cache.purge() assert cache.last_found_secrets == list() with my_vcr.use_cassette("multiple_secrets"): c.scan( client=client, cache=cache, matches_ignore=config.matches_ignore, all_policies=True, verbose=False, ) assert config.matches_ignore == list() cache_found_secrets = sorted(cache.last_found_secrets, key=compare_matches_ignore) found_secrets = sorted(FOUND_SECRETS, key=compare_matches_ignore) assert [found_secret["match"] for found_secret in cache_found_secrets] == [ found_secret["match"] for found_secret in found_secrets ] ignore_last_found(config, cache) for ignore in config.matches_ignore: assert "test.txt" in ignore["name"] cache.load_cache()
def test_exclude_regex(self, cli_fs_runner, local_config_path, monkeypatch): write_yaml(local_config_path, {"paths-ignore": ["/tests/"]}) monkeypatch.setattr("ggshield.core.config.GLOBAL_CONFIG_FILENAMES", []) config = Config() assert r"/tests/" in config.paths_ignore
def test_unknown_option(self, cli_fs_runner, capsys, local_config_path, monkeypatch): write_yaml(local_config_path, {"verbosity": True}) monkeypatch.setattr("ggshield.core.config.GLOBAL_CONFIG_FILENAMES", []) Config() captured = capsys.readouterr() assert "Unrecognized key in config" in captured.out
def test_display_options(self, cli_fs_runner, local_config_path, monkeypatch): write_yaml(local_config_path, {"verbose": True, "show_secrets": True}) monkeypatch.setattr("ggshield.core.config.GLOBAL_CONFIG_FILENAMES", []) config = Config() assert config.verbose is True assert config.show_secrets is True
def test_timezone_aware_expired(self): """ GIVEN a config with a configured instance WHEN loading the config THEN the instance expiration date is timezone aware """ write_yaml(get_auth_config_filepath(), self.default_config) config = Config() assert config.instances["default"].account.expire_at.tzinfo is not None
def test_load_file_not_existing(self): """ GIVEN the auth config file not existing WHEN loading the config THEN it works and has the default configuration """ config = Config() assert config.default_instance == "https://dashboard.gitguardian.com" assert config.default_token_lifetime is None assert config.instances == {}
def test_max_commits_for_hook_setting(self, cli_fs_runner): """ GIVEN a yaml config with `max-commits-for-hook=75` WHEN the config gets parsed THEN the default value of max_commits_for_hook (50) should be replaced with 75 """ with open(".gitguardian.yml", "w") as file: file.write(yaml.dump({"max-commits-for-hook": 75})) config = Config() assert config.max_commits_for_hook == 75
def test_ignore_last_found_with_manually_added_secrets(client, isolated_fs): """ GIVEN a cache containing part of config ignored-matches secrets WHEN I run ignore command THEN only new discovered secrets are added to the config """ manually_added_secret = ( "41b8889e5e794b21cb1349d8eef1815960bf5257330fd40243a4895f26c2b5c8" ) config = Config() config.matches_ignore = [{"name": "", "match": manually_added_secret}] cache = Cache() cache.last_found_secrets = FOUND_SECRETS ignore_last_found(config, cache) matches_ignore = sorted(config.matches_ignore, key=compare_matches_ignore) found_secrets = sorted(FOUND_SECRETS, key=compare_matches_ignore) assert matches_ignore == found_secrets
def _assert_config(token=None): """ assert that the config exists. If a token is passed, assert that the token saved in the config is the same """ config = Config() assert len(config.auth_config.instances) == 1 assert config.auth_config.default_instance in config.auth_config.instances if token is not None: assert (config.auth_config.instances[ config.auth_config.default_instance].account.token == token)
def test_instance_name_priority( self, current_instance, env_instance, local_instance, global_instance, default_instance, expected_instance, local_config_path, global_config_path, ): """ GIVEN different instances defined in the different possible sources: - manually set on the config - env variable - local user config - global user config - default instance in the auth config WHEN reading the config instance THEN it respects the expected priority """ if env_instance: os.environ["GITGUARDIAN_URL"] = env_instance elif "GITGUARDIAN_URL" in os.environ: del os.environ["GITGUARDIAN_URL"] if "GITGUARDIAN_API_URL" in os.environ: del os.environ["GITGUARDIAN_API_URL"] self.set_instances( local_instance=local_instance, global_instance=global_instance, default_instance=default_instance, local_filepath=local_config_path, global_filepath=global_config_path, ) config = Config() config.current_instance = current_instance assert config.instance_name == expected_instance assert config.dashboard_url == expected_instance assert config.api_url == dashboard_to_api_url(expected_instance)
def test_token_not_expiring(self): """ GIVEN an auth config file with a token never expiring WHEN loading the AuthConfig THEN it works """ raw_config = deepcopy(self.default_config) raw_config["instances"]["default"]["accounts"][0]["expire-at"] = None write_yaml(get_auth_config_filepath(), raw_config) config = Config() assert config.instances["default"].account.expire_at is None
def test_parsing_error(cli_fs_runner, capsys, monkeypatch, tmp_path): filepath = os.path.join(tmp_path, "test_local_gitguardian.yml") monkeypatch.setattr("ggshield.core.config.LOCAL_CONFIG_PATHS", [filepath]) monkeypatch.setattr("ggshield.core.config.GLOBAL_CONFIG_FILENAMES", []) write_text(filepath, "Not a:\nyaml file.\n") Config() out, err = capsys.readouterr() sys.stdout.write(out) sys.stderr.write(err) assert f"Parsing error while reading {filepath}:" in out
def test_v1_in_api_url_env(self, capsys, monkeypatch): """ GIVEN an API URL ending with /v1 configured via env var WHEN loading the config THEN writes a warning to stderr """ monkeypatch.setitem(os.environ, "GITGUARDIAN_API_URL", "https://api.gitguardian.com/v1") Config() out, err = capsys.readouterr() sys.stdout.write(out) sys.stderr.write(err) assert "[Warning] unexpected /v1 path in your URL configuration" in err
def test_ignore_last_found_compatible_with_previous_matches_ignore_format( client, isolated_fs ): """ GIVEN a cache containing new secrets AND a config's matches_ignore not empty as a list of strings WHEN I run ignore command THEN config's matches_ignore is updated AND strings hashes are unchanged """ config = Config() old_format_matches_ignore = [ "some_secret_hash", "another_secret_hash", ] config.matches_ignore = old_format_matches_ignore.copy() cache = Cache() cache.last_found_secrets = FOUND_SECRETS ignore_last_found(config, cache) assert sorted(config.matches_ignore, key=compare_matches_ignore) == sorted( FOUND_SECRETS + old_format_matches_ignore, key=compare_matches_ignore )
def test_user_confi_url_no_configured_instance(self): """ GIVEN a bare auth config, but urls configured in the user config WHEN reading api_url/dashboard_url THEN it works """ config = Config() assert config.auth_config.instances == {} # from the default test env vars: assert config.api_url == "https://api.gitguardian.com" assert config.dashboard_url == "https://dashboard.gitguardian.com"
def test_invalid_format(self, capsys): """ GIVEN an auth config file with invalid content WHEN loading AuthConfig THEN it raises """ write_text(get_auth_config_filepath(), "Not a:\nyaml file.\n") Config() out, err = capsys.readouterr() sys.stdout.write(out) sys.stderr.write(err) assert f"Parsing error while reading {get_auth_config_filepath()}:" in out
def test_assert_flow_enabled( self, monkeypatch, version, preference_enabled, status_code, expected_error, ): """ GIVEN - WHEN checking the availability of the ggshield auth flow web method on a dashboard of various various, with or without the flow enabled THEN it succeeds if the version is high enough, and the preference is enabled """ def client_get_mock(*args, endpoint, **kwargs): if endpoint == "metadata": return Mock( ok=status_code < 400, status_code=status_code, json=lambda: { "version": version, "preferences": { "public_api__ggshield_auth_flow_enabled": preference_enabled } if preference_enabled is not None else {}, }, ) raise NotImplementedError monkeypatch.setattr("ggshield.core.client.GGClient.get", client_get_mock) if expected_error: with pytest.raises(ClickException, match=expected_error): check_instance_has_enabled_flow(Config()) else: check_instance_has_enabled_flow(Config())
def test_load(self): """ GIVEN a default auth config WHEN loading the config THEN when serializing it again, it matches the data. """ write_yaml(get_auth_config_filepath(), self.default_config) config = Config() assert config.instances["default"].account.token_name == "my_token" config_data = config.auth_config.to_dict() replace_in_keys(config_data, old_char="_", new_char="-") assert config_data == self.default_config
def test_update(self): """ GIVEN - WHEN modifiying the default config THEN it's not persisted until .save() is called """ config = Config() config.default_instance = "custom" assert Config().default_instance != "custom" config.save() assert Config().default_instance == "custom"
def test_no_account(self, n): """ GIVEN an auth config with a instance with 0 or more than 1 accounts WHEN loading the AuthConfig THEN it raises """ raw_config = deepcopy(self.default_config) raw_config["instances"]["default"]["accounts"] = ( raw_config["instances"]["default"]["accounts"] * n) write_yaml(get_auth_config_filepath(), raw_config) with pytest.raises( AssertionError, match= "Each GitGuardian instance should have exactly one account", ): Config()
def test_auth_login_token(self, monkeypatch, cli_fs_runner, test_case): """ GIVEN an API token, valid or not WHEN the auth login command is called with --method=token THEN the validity of the token should be checked, and if valid, the user should be logged in """ token = "mysupertoken" instance = "https://dashboard.gitguardian.com" cmd = ["auth", "login", "--method=token", f"--instance={instance}"] if test_case == "valid": self.mock_autho_login_request(monkeypatch, 200, self.VALID_TOKEN_PAYLOAD) elif test_case == "invalid_scope": self.mock_autho_login_request( monkeypatch, 200, self.VALID_TOKEN_INVALID_SCOPE_PAYLOAD) elif test_case == "invalid": self.mock_autho_login_request(monkeypatch, 401, self.INVALID_TOKEN_PAYLOAD) check_instance_has_enabled_flow_mock = Mock() monkeypatch.setattr( "ggshield.cmd.auth.login.check_instance_has_enabled_flow", check_instance_has_enabled_flow_mock, ) result = cli_fs_runner.invoke(cli, cmd, color=False, input=token + "\n") config = Config() if test_case == "valid": assert result.exit_code == 0, result.output assert instance in config.auth_config.instances assert config.auth_config.instances[ instance].account.token == token else: assert result.exit_code != 0 if test_case == "invalid_scope": assert "This token does not have the scan scope." in result.output else: assert "Authentication failed with token." in result.output assert instance not in config.auth_config.instances check_instance_has_enabled_flow_mock.assert_not_called()
def cli( ctx: click.Context, config_path: Optional[str], verbose: bool, allow_self_signed: bool, ) -> None: load_dot_env() ctx.ensure_object(dict) ctx.obj["config"] = Config(config_path) ctx.obj["cache"] = Cache() if verbose is not None: ctx.obj["config"].verbose = verbose if allow_self_signed is not None: ctx.obj["config"].allow_self_signed = allow_self_signed
def test_ignore_last_found(client, isolated_fs): """ GIVEN a cache of last found secrets not empty WHEN I run a ignore last found command THEN config ignored-matches is updated accordingly """ config = Config() setattr(config, "matches_ignore", list()) cache = Cache() cache.last_found_secrets = FOUND_SECRETS ignore_last_found(config, cache) matches_ignore = sorted(config.matches_ignore, key=compare_matches_ignore) found_secrets = sorted(FOUND_SECRETS, key=compare_matches_ignore) assert matches_ignore == found_secrets assert cache.last_found_secrets == FOUND_SECRETS
def test_v1_in_api_url_global_config(self, capsys, global_config_path): """ GIVEN an API URL ending with /v1 configured in the global config file WHEN loading the config THEN writes a warning to stderr """ write_yaml( global_config_path, { "verbose": False, "show_secrets": True, "api_url": "https://api.gitguardian.com/v1", }, ) Config() out, err = capsys.readouterr() sys.stdout.write(out) sys.stderr.write(err) assert "[Warning] unexpected /v1 path in your URL configuration" in err