Repository: RedHatProductSecurity/rapidast

Issue:
Access tokens are printed in results file
At least with the Garak integration, if the user writes an access token inside the configuration file, that token is later showed in the results files as part of the garak configuration:

```
RestGenerator:
              headers:
                Authorization: Bearer <redacted>
                Content-Type: application/json
                Referer: XXXXX
```

In other section of the configuration files, such as the `general/authentication`, the token is obfuscated at the time of printing the results:

```
general:
  authentication:
    parameters:
      password: '*****'
      username: '*****'
    type: http_basic
```

I don't know if that's an expected behavior, but I can imagine that for users with public pipelines and artifacts, they don't want their token to be published as part of the results files. The tokens inside Garak should also be obfuscated in order to enhance the security of our pipelines.

Discussion:
@jeremychoi: I agree that will be necessary.
Currently, it's a known issue. It's applied only to the items of `authentication.parameters`. In the case of Garak, the generator config can have arbitrary values and the auth is setup there , so it isn't applied automatically now.

Solution patch:
diff --git a/rapidast.py b/rapidast.py
index 5f963dc..c635000 100755
--- a/rapidast.py
+++ b/rapidast.py
@@ -30,6 +30,22 @@ pp = pprint.PrettyPrinter(indent=4)
 
 
 DEFAULT_CONFIG_FILE = os.path.join(os.path.dirname(__file__), "rapidast-defaults.yaml")
+SENSITIVE_KEYS = {
+    "password",
+    "secret",
+    "api_key",
+    "token",
+    "access_key",
+    "private_key",
+    "authorization",
+    "proxy-authorization",
+    "cookie",
+    "x-api-key",
+    "x-auth-token",
+    "x-csrf",
+    "x-csrf-token",
+    "x-xsrf-token",
+}
 
 
 def load_environment(config):
@@ -157,7 +173,7 @@ def run_scanner(name, config, args, scan_exporter):
 
 def dump_redacted_config(config_file_location: str, destination_dir: str) -> bool:
     """
-    Redacts sensitive parameters from a configuration file and writes the redacted
+    Redacts sensitive parameters and values from a configuration file and writes the redacted
     version to a destination directory
 
     Args:
@@ -167,6 +183,27 @@ def dump_redacted_config(config_file_location: str, destination_dir: str) -> boo
     """
     logging.info(f"Starting the redaction and dumping process for the configuration file: {config_file_location}")
 
+    mask_value_str = "*****"
+
+    def _mask_sensitive_data(data):
+        """Recursively mask sensitive values in a dictionary or list."""
+
+        if isinstance(data, dict):
+            masked_data = {}
+            for key, value in data.items():
+                if key == "authentication" and isinstance(value, dict) and "parameters" in value:
+                    # Mask all values inside "authentication" -> "parameters"
+                    masked_data[key] = value.copy()
+                    masked_data[key]["parameters"] = {param: mask_value_str for param in value["parameters"]}
+                elif key.lower() in SENSITIVE_KEYS:
+                    masked_data[key] = mask_value_str
+                else:
+                    masked_data[key] = _mask_sensitive_data(value)  # Recurse for nested structures
+            return masked_data
+        elif isinstance(data, list):
+            return [_mask_sensitive_data(item) for item in data]  # Recurse for lists
+        return data
+
     try:
         if not os.path.exists(destination_dir):
             os.makedirs(destination_dir)
@@ -175,17 +212,12 @@ def dump_redacted_config(config_file_location: str, destination_dir: str) -> boo
         config = yaml.safe_load(load_config_file(config_file_location))
 
         logging.info(f"Redacting sensitive information from configuration {config_file_location}")
-        for key in config.keys():
-            if not isinstance(config[key], dict):
-                continue
-            if config[key].get("authentication") and config[key]["authentication"].get("parameters"):
-                for param in config[key]["authentication"]["parameters"]:
-                    config[key]["authentication"]["parameters"][param] = "*****"
+        redacted_config = _mask_sensitive_data(config)
 
         dest = os.path.join(destination_dir, os.path.basename(config_file_location))
         logging.info(f"Saving redacted configuration to {dest}")
         with open(dest, "w", encoding="utf-8") as file:
-            yaml.dump(config, file)
+            yaml.dump(redacted_config, file)
 
         logging.info("Redacted configuration saved successfully")
         return True


Test patch:
diff --git a/tests/test_dump_redacted_config.py b/tests/test_dump_redacted_config.py
index 0dc9c45..6214f2c 100644
--- a/tests/test_dump_redacted_config.py
+++ b/tests/test_dump_redacted_config.py
@@ -14,6 +14,8 @@ def mock_yaml_data() -> dict:
     return {
         "service1": {"authentication": {"parameters": {"username": "admin", "password": "secret"}}},
         "service2": {"authentication": {"parameters": {"api_key": "123456"}}},
+        "service3": {"RestGenerator": {"headers": {"Authorization": "token1234"}}},
+        "service4": {"Garak": {"RestGenerator": {"headers": {"Cookie": "session=token1234"}}}},
     }
 
 
@@ -27,6 +29,8 @@ def test_dump_redacted_config_success(
     expected_redacted_data = {
         "service1": {"authentication": {"parameters": {"username": "*****", "password": "*****"}}},
         "service2": {"authentication": {"parameters": {"api_key": "*****"}}},
+        "service3": {"RestGenerator": {"headers": {"Authorization": "*****"}}},
+        "service4": {"Garak": {"RestGenerator": {"headers": {"Cookie": "*****"}}}},
     }
     mock_yaml_load.return_value = mock_yaml_data
     success = dump_redacted_config("config.yaml", "destination_dir")