Repository: gitguardian/ggshield
Issue: Config Error Messages Replace "-" With "_"

## Environment

- ggshield version: 1.14.3
- Operating system (Linux, macOS, Windows): macOS
- Operating system version: Ventura 13.1
- Python version: 3.10.10

## Describe the bug

The "Unrecognized key in config" error message displays an underscore instead of hyphen when an invalid configuration is present.

Steps to reproduce:

Create an invalid configuration file such as (note `ignored-matches` misspelled):

**`.gitguardian.yml`**
```yaml
version: 2
secret:
  ignore-matches:
    - name: some-secret-i-want-to-ignore
      match: 4f2f11652a0<snip>5ead42463
```

Execute a `git commit` that includes the secret you wish to ignore (with GGShield configured as a pre-commit hook).

Actual result:

Error message:

> Unrecognized key in config: ignore_matches

Note the underscore when the configuration has a hyphen.

Expected result:

Error message:

> Unrecognized key in config: ignore-matches

(hyphen instead of underscore)


Changed files:
tests/unit/core/config/test_user_config.py, ggshield/core/config/auth_config.py, ggshield/core/config/utils.py, ggshield/core/types.py, ggshield/core/config/user_config.py

diff --git a/ggshield/core/config/auth_config.py b/ggshield/core/config/auth_config.py
index 8a5189a..923931e 100644
--- a/ggshield/core/config/auth_config.py
+++ b/ggshield/core/config/auth_config.py
@@ -9,6 +9,7 @@ from ggshield.core.config.utils import (
     ensure_path_exists,
     get_auth_config_filepath,
     load_yaml_dict,
+    replace_in_keys,
     save_yaml_dict,
 )
 from ggshield.core.dirs import get_config_dir
@@ -68,7 +69,10 @@ def prepare_auth_config_dict_for_parse(data: Dict[str, Any]) -> Dict[str, Any]:
     single instances[].account key.
 
     Stop if there is not exactly one account.
+
+    We replace `-` with `_` for compatibility reasons.
     """
+    replace_in_keys(data, "-", "_")
     data = deepcopy(data)
     try:
         instances = data["instances"]
diff --git a/ggshield/core/config/user_config.py b/ggshield/core/config/user_config.py
index 86d98dc..9d28aae 100644
--- a/ggshield/core/config/user_config.py
+++ b/ggshield/core/config/user_config.py
@@ -10,6 +10,7 @@ from ggshield.core.config.utils import (
     get_global_path,
     load_yaml_dict,
     remove_common_dict_items,
+    replace_in_keys,
     save_yaml_dict,
     update_from_other_instance,
 )
@@ -113,6 +114,7 @@ class UserConfig(FilteredConfig):
 
         dct["version"] = CURRENT_CONFIG_VERSION
 
+        replace_in_keys(dct, old_char="_", new_char="-")
         save_yaml_dict(dct, config_path)
 
     @classmethod
@@ -160,6 +162,7 @@ class UserConfig(FilteredConfig):
             if config_version == 2:
                 obj = UserConfigSchema().load(data)
             elif config_version == 1:
+                replace_in_keys(data, old_char="-", new_char="_")
                 self.deprecation_messages.append(
                     f"{config_path} uses a deprecated configuration file format."
                     " Run `ggshield config migrate` to migrate it to the latest version."
diff --git a/ggshield/core/config/utils.py b/ggshield/core/config/utils.py
index 4653386..555c0b7 100644
--- a/ggshield/core/config/utils.py
+++ b/ggshield/core/config/utils.py
@@ -37,12 +37,10 @@ def load_yaml_dict(path: str) -> Optional[Dict[str, Any]]:
     if not isinstance(data, dict):
         raise ValueError(f"{path} should be a dictionary.")
 
-    replace_in_keys(data, old_char="-", new_char="_")
     return data
 
 
 def save_yaml_dict(data: Dict[str, Any], path: str) -> None:
-    replace_in_keys(data, "_", "-")
     p = Path(path)
     p.parent.mkdir(parents=True, exist_ok=True)
     with p.open("w") as f:
diff --git a/ggshield/core/types.py b/ggshield/core/types.py
index fe76e6f..a604ca4 100644
--- a/ggshield/core/types.py
+++ b/ggshield/core/types.py
@@ -18,8 +18,9 @@ class FilteredConfig:
         field_names = {field_.name for field_ in fields(cls)}
         filtered_fields = {}
         for key, item in data.items():
-            if key in field_names:
-                filtered_fields[key] = item
+            filtered_key = key.replace("-", "_")
+            if filtered_key in field_names:
+                filtered_fields[filtered_key] = item
             else:
                 display_warning("Unrecognized key in config: {}".format(key))
 
diff --git a/tests/unit/core/config/test_user_config.py b/tests/unit/core/config/test_user_config.py
index b4cb7e8..e6977de 100644
--- a/tests/unit/core/config/test_user_config.py
+++ b/tests/unit/core/config/test_user_config.py
@@ -239,9 +239,15 @@ class TestUserConfig:
                 "secret": {
                     "secret_invalid_key": "invalid key",
                     "ignored-matches": [
-                        {"name": "", "match": "one", "match_invalid_key": "two"},
+                        {
+                            "name": "",
+                            "match": "one",
+                            "match_invalid_key": "two",
+                            "nested-hashed": "example",
+                        },
                     ],
                 },
+                "hashed-key": "example",
             },
         )
         UserConfig.load(local_config_path)
@@ -250,3 +256,5 @@ class TestUserConfig:
         assert "Unrecognized key in config: iac_unknown" in captured.err
         assert "Unrecognized key in config: secret_invalid_key" in captured.err
         assert "Unrecognized key in config: match_invalid_key" in captured.err
+        assert "Unrecognized key in config: hashed-key" in captured.err
+        assert "Unrecognized key in config: nested-hashed" in captured.err