Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mask TOML secrets action #154

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions chainlink-testing-framework/mask-toml-secrets/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Mask TOML Secrets Action

This GitHub Action masks all secret values in Base64-encoded TOML configurations within GitHub Actions logs. It is designed to ensure that sensitive information denoted by keys ending with `_secret` is not exposed in Github logs.

## Inputs

### `base64_toml_content`

- **Description**: Base64-encoded TOML configurations separated by spaces that contain "_secret" keys to be masked.
- **Required**: Yes

## How to Use

To use this action in your workflow, follow the steps below:

1. Ensure you have a Base64-encoded TOML configuration string that you wish to mask. The keys for any secrets in the TOML should end with `_secret`.
2. Add a step in your GitHub Actions workflow to use this action. Do it **before** any other action related to the TOML
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe give more examples?

  • if that toml is user input, when should this action run?
  • if that toml is generated in the workflow, when should this action run?

Copy link
Contributor Author

@lukaszcl lukaszcl Feb 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tofel

if that toml is user input, when should this action run?

the action should run before you use TOML values. This is in the readme. Please say how would you want to update this text :)

if that toml is generated in the workflow, when should this action run?

This action does not care how you generate base64-encoded toml. You just have to pass it to the action if you want to mask keys with _secret suffix

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant: add it to the readme!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to see specific examples for these 2 cases, readme is too abstract for me, I wouldn't be sure how to use that acitons in these 2 cases


Example:

```yaml
steps:
- name: Mask TOML Secrets
uses: smartcontractkit/chainlink-github-actions/chainlink-testing-framework/mask-toml-secrets@main
with:
base64_toml_content: >
${{ secrets.DOCKER_TESTS_BASE64_TOML_CONTENT_1 }}
${{ secrets.DOCKER_TESTS_BASE64_TOML_CONTENT_2 }}
```
24 changes: 24 additions & 0 deletions chainlink-testing-framework/mask-toml-secrets/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: 'Mask TOML Secrets'
description: 'Masks all secret values in Base64-encoded TOML configurations within GitHub Actions logs'
inputs:
base64_toml_content:
description: 'Base64-encoded TOML configurations separated by spaces that contain "_secret" keys to be masked'
required: true
runs:
using: 'composite'
steps:
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.x'

- run: pip install toml
shell: bash

- name: Run mask script tests
run: python3 ${{ github.action_path }}/mask_toml_secrets.py --test
shell: bash
lukaszcl marked this conversation as resolved.
Show resolved Hide resolved

- name: Mask secrets in TOML
run: python ${{ github.action_path }}/mask_toml_secrets.py ${{ inputs.base64_toml_content }}
shell: bash
110 changes: 110 additions & 0 deletions chainlink-testing-framework/mask-toml-secrets/mask_toml_secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import toml
import base64
import sys
import unittest

def find_secret_values(obj):
secret_values = []

if isinstance(obj, dict):
for key, value in obj.items():
if isinstance(value, (dict, list)):
if key.endswith("_secret") and isinstance(value, list):
# Directly add all items in the list if the key ends with '_secret'
secret_values.extend(value)
else:
# Recursively search within the dict or list
secret_values.extend(find_secret_values(value))
elif key.endswith("_secret"):
secret_values.append(value)
elif isinstance(obj, list):
for item in obj:
secret_values.extend(find_secret_values(item))

return secret_values

# Main execution
def main():
if len(sys.argv) < 2:
print("Usage: python mask_toml_secrets.py <base64_toml1> <base64_toml2> ...")
sys.exit(1)

print(f"Provided {len(sys.argv)-1} base64_toml")

all_secrets = []
for encoded_data in sys.argv[1:]:
try:
decoded_data = base64.b64decode(encoded_data).decode("utf-8")
data = toml.loads(decoded_data)
if not data: # Check if the data is empty
raise ValueError("TOML config is empty")
except Exception as e:
raise Exception(f"Could not decode TOML config. Error: {e}") from None
secrets = find_secret_values(data)
all_secrets.extend(secrets)

for secret in all_secrets:
print(f"::add-mask::{secret}")

print(f"Masked {len(all_secrets)} secrets")

# Test cases
class TestSecretValueFinder(unittest.TestCase):
def test_find_secret_values(self):
tests = [
{
"name": "Single secret",
"input": {"api_secret": "12345"},
"expectedSecrets": ["12345"],
},
{
"name": "Multiple secrets",
"input": {
"api_secret": "12345",
"nested": {
"db_secret": "abcde",
"nested_list": [
{"1_secret": "abc"},
{"second_secret": "def"},
{"api_key": "g"},
],
},
},
"expectedSecrets": ["12345", "abcde", "abc", "def"],
},
{
"name": "Multiple mixed type secrets",
"input": {
"string_secret": "secret",
"int_secret": 123,
"bool_secret": True,
"float_secret": 3.14,
"nested": {"nested_secret": "nested"},
},
"expectedSecrets": ["secret", "123", "True", "3.14", "nested"],
},
{
"name": "List secrets",
"input": {
"nested": {"nested_secret": ["abc", True, 3.14]},
},
"expectedSecrets": ["3.14", "True", "abc"],
},
{
"name": "No secret",
"input": {"api_key": "abcde"},
"expectedSecrets": [],
},
]

for test in tests:
with self.subTest(name=test["name"]):
result = find_secret_values(test["input"])
self.assertEqual(sorted(map(str, result)), sorted(map(str, test["expectedSecrets"])))

if __name__ == '__main__':
if '--test' in sys.argv:
sys.argv.remove('--test') # Remove the test argument before running unittest
unittest.main()
else:
main()