예제 #1
0
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 == []
예제 #2
0
    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)
예제 #3
0
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
예제 #4
0
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)
예제 #5
0
    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)
예제 #6
0
    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
예제 #7
0
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()
예제 #8
0
    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
예제 #9
0
    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
예제 #10
0
    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
예제 #11
0
 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
예제 #12
0
    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 == {}
예제 #13
0
    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
예제 #14
0
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
예제 #15
0
 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)
예제 #16
0
    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)
예제 #17
0
    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
예제 #18
0
    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
예제 #19
0
    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
예제 #20
0
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
    )
예제 #21
0
    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"
예제 #22
0
    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
예제 #23
0
    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())
예제 #24
0
    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
예제 #25
0
    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"
예제 #26
0
    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()
예제 #27
0
    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()
예제 #28
0
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
예제 #29
0
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
예제 #30
0
    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