diff --git a/action.yml b/action.yml index 8718934..ad09b57 100644 --- a/action.yml +++ b/action.yml @@ -23,6 +23,14 @@ inputs: description: 'A specific repo to sync the secrets to' mandatory: False default: '' + dependabotSecretsSync: + description: 'Sync secrets to dependabot (yes/no), default no' + mandatory: False + default: 'no' + repo_owner: + description: 'Repository Owner Name' + mandatory: False + default: 'org-name' runs: using: 'docker' image: 'Dockerfile' @@ -33,6 +41,8 @@ runs: NAMES: ${{ inputs.secret-names }} VALUES: ${{ inputs.secret-values }} REPO: ${{ inputs.repo }} + DEPENDABOTSECRETSYNC: ${{ inputs.dependabotSecretsSync}} + REPOSITORYOWNER: ${{ inputs.repo_owner }} branding: icon: 'lock' color: 'blue' diff --git a/entrypoint.sh b/entrypoint.sh index b681bb8..4f86cd8 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,13 +2,13 @@ if [ ${#TEAM} -gt 0 ] && [ ${#REPO} -gt 0 ] then - python /app/main.py $ACTION --token $TOKEN --names $NAMES --values $VALUES --team $TEAM --repo $REPO + python /app/main.py $ACTION --token $TOKEN --names $NAMES --values $VALUES --team $TEAM --repo $REPO --dependabotsecretssync $DEPENDABOTSECRETSYNC --repoowner $REPOSITORYOWNER elif [ ${#TEAM} -gt 0 ] then - python /app/main.py $ACTION --token $TOKEN --names $NAMES --values $VALUES --team $TEAM + python /app/main.py $ACTION --token $TOKEN --names $NAMES --values $VALUES --team $TEAM --dependabotsecretssync $DEPENDABOTSECRETSYNC --repoowner $REPOSITORYOWNER elif [ ${#REPO} -gt 0 ] then - python /app/main.py $ACTION --token $TOKEN --names $NAMES --values $VALUES --repo $REPO + python /app/main.py $ACTION --token $TOKEN --names $NAMES --values $VALUES --repo $REPO --dependabotsecretssync $DEPENDABOTSECRETSYNC --repoowner $REPOSITORYOWNER else - python /app/main.py $ACTION --token $TOKEN --names $NAMES --values $VALUES -fi + python /app/main.py $ACTION --token $TOKEN --names $NAMES --values $VALUES --dependabotsecretssync $DEPENDABOTSECRETSYNC --repoowner $REPOSITORYOWNER +fi \ No newline at end of file diff --git a/main.py b/main.py index 93a0423..e8fdabd 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,12 @@ import sys - +import json import requests from github import Github, BadCredentialsException from github.GithubException import UnknownObjectException +from base64 import b64encode +from nacl import encoding, public + args = sys.argv[1:] source = None @@ -15,7 +18,10 @@ valuesCommand = "--values" teamCommand = "--team" repoCommand = "--repo" +dependabotSecretsSyncOption = "--dependabotsecretssync" +repositoryOwner = "--repoowner" interactiveCommand = "--interactive" +defaultRepoOwnerName = "org-name" noTokenMessage = "Please provide a valid GitHub PAT using --token ." invalidTokenMessage = "The token you provided is invalid." @@ -25,16 +31,19 @@ invalidNamesAndSecretsMessage = "Secret names and secret values lists are not the same length. This may be due to an " \ "invalid input or a secret that contains a comma. Secrets with comma(s) are currently" \ " not supported." +invalidRepoOwnerMessage = "Invalid Repository Owner name, please pass the value to sync dependabot secrets!" class UserInput: - def __init__(self, token, action, secret_names, secret_values, target_team_name, target_repo_name, interactive): + def __init__(self, token, action, secret_names, secret_values, target_team_name, target_repo_name, dependabotSecretsSync, repoOwner, interactive): self.token = token self.action = action self.secret_names = secret_names self.secret_values = secret_values self.target_team_name = target_team_name self.target_repo_name = target_repo_name + self.dependabotSecretsSync = dependabotSecretsSync + self.repoOwner = repoOwner self.interactive = interactive @@ -121,9 +130,11 @@ def get_input_from_cli(): secret_values = get_optional_value_from_input(args, valuesCommand).split(',') target_team_name = get_optional_value_from_input(args, teamCommand) target_repo_name = get_optional_value_from_input(args, repoCommand) + dependabotSecretsSync = get_optional_value_from_input(args, dependabotSecretsSyncOption) + repoOwner = get_optional_value_from_input(args, repositoryOwner) interactive = interactiveCommand in args action = validate_action(args[0], createCommand, updateCommand, deleteCommand, secret_names, secret_values) - return UserInput(token, action, secret_names, secret_values, target_team_name, target_repo_name, interactive) + return UserInput(token, action, secret_names, secret_values, target_team_name, target_repo_name, dependabotSecretsSync, repoOwner, interactive) def flatten_secrets_dict(dict_of_secrets): @@ -132,6 +143,21 @@ def flatten_secrets_dict(dict_of_secrets): list_of_secrets.append(secret["name"]) return list_of_secrets +def get_repo_public_key(token, repo_owner, repo_name): + + query_url = f"https://github.com/gitapi/repos/{repo_owner}/{repo_name}/dependabot/secrets/public-key" + headers = {'Authorization': f'token {token}'} + r = requests.get(query_url, headers=headers) + response = r.json() + return response["key_id"], response["key"] + +def encrypt(public_key: str, secret_value: str) -> str: + """Encrypt a Unicode string using the public key.""" + public_key = public.PublicKey(public_key.encode("utf-8"), encoding.Base64Encoder()) + sealed_box = public.SealedBox(public_key) + encrypted = sealed_box.encrypt(secret_value.encode("utf-8")) + return b64encode(encrypted).decode("utf-8") + def add_secret(token, target_repository, secret_name, secret_value): repo_full_name = target_repository.full_name @@ -150,12 +176,81 @@ def add_secret(token, target_repository, secret_name, secret_value): else: print(f"Secret \"{secret_name}\" already exists in {repo_name}") +def add_dependabot_secret(token, target_repository, secret_name, secret_value, repoOwner): + repo_name = target_repository.name + repo_owner = repoOwner + key_id, key = get_repo_public_key(token, repo_owner, repo_name) + query_url = f"https://github.com/gitapi/repos/{repo_owner}/{repo_name}/dependabot/secrets" + headers = {'Authorization': f'token {token}'} + r = requests.get(query_url, headers=headers) + response = r.json() + try: + secret_names = flatten_secrets_dict(response["secrets"]) + except: + secret_names = [] + if secret_name not in secret_names: + # put call add repo secrets to dependabot secrets + url = f"https://github.com/gitapi/repos/{repo_owner}/{repo_name}/dependabot/secrets/{secret_name}" + + data = { + "encrypted_value": encrypt(key, secret_value), + "key_id": key_id + } + response = requests.put(url, headers=headers, data=json.dumps(data)) + print(f"Response Code: {response.status_code}") + if response.status_code == 201: + print(f"dependabot Secret \"{secret_name}\" added to {repo_name}") + else: + print(f"dependabot Secret \"{secret_name}\" could NOT be added to {repo_name}") + else: + print(f"dependabot Secret \"{secret_name}\" already exists in {repo_name}") + +def update_dependabot_secret(token, target_repository, secret_name, secret_value, repoOwner): + repo_name = target_repository.name + repo_owner = repoOwner + key_id, key = get_repo_public_key(token, repo_owner, repo_name) + query_url = f"https://github.com/gitapi/repos/{repo_owner}/{repo_name}/dependabot/secrets" + headers = {'Authorization': f'token {token}'} + r = requests.get(query_url, headers=headers) + response = r.json() + try: + secret_names = flatten_secrets_dict(response["secrets"]) + except: + secret_names = [] + if secret_name not in secret_names: + # patch call update repo secrets to dependabot secrets + url = f"https://github.com/gitapi/repos/{repo_owner}/{repo_name}/dependabot/secrets/{secret_name}" + + data = { + "encrypted_value": encrypt(key, secret_value), + "key_id": key_id + } + response = requests.patch(url, headers=headers, data=json.dumps(data)) + print(f"Response Code: {response.status_code}") + if response.status_code == 204: + print(f"dependabot Secret \"{secret_name}\" updated in {repo_name}") + else: + print(f"dependabot Secret \"{secret_name}\" could NOT be updated in {repo_name}") + +def delete_dependabot_secret(token, target_repository, secret_name, repoOwner): + repo_name = target_repository.name + repo_owner = repoOwner + headers = {'Authorization': f'token {token}'} + # put call add repo secrets to dependabot secrets + url = f"https://github.com/gitapi/repos/{repo_owner}/{repo_name}/dependabot/secrets/{secret_name}" + response = requests.delete(url, headers=headers) + print(f"Response Code: {response.status_code}") + if response.status_code == 204: + print(f"dependabot Secret \"{secret_name}\" deleted from {repo_name}") + else: + print(f"dependabot Secret \"{secret_name}\" could NOT be deleted from {repo_name}") if __name__ == "__main__": if len(args) == 0: inp = get_input_from_user() else: inp = get_input_from_cli() + print(inp.repoOwner) g = get_github_user(inp.token, invalidTokenMessage) @@ -173,16 +268,43 @@ def add_secret(token, target_repository, secret_name, secret_value): continue for i in range(len(inp.secret_names)): - if not inp.interactive or apply_action(repo.name): - try: - if inp.action == createCommand: - add_secret(inp.token, repo, inp.secret_names[i], inp.secret_values[i]) - if inp.action == updateCommand: - c = repo.get_contributors() - repo.create_secret(inp.secret_names[i], inp.secret_values[i]) - print(f"Secret \"{inp.secret_names[i]}\" updated for {repo.name}") - if inp.action == deleteCommand: - repo.delete_secret(inp.secret_names[i]) - print(f"Secret \"{inp.secret_names[i]}\" removed from {repo.name}") - except UnknownObjectException: - print(f"The provided token does not have permission to manage {repo.name}, it is being skipped") + if len(inp.dependabotSecretsSync) == 0 or inp.dependabotSecretsSync.lower() == "yes" or inp.dependabotSecretsSync.lower() == '': + if not inp.interactive or apply_action(repo.name): + try: + if inp.action == createCommand: + add_secret(inp.token, repo, inp.secret_names[i], inp.secret_values[i]) + if not inp.repoOwner == defaultRepoOwnerName: + add_dependabot_secret(inp.token, repo, inp.secret_names[i], inp.secret_values[i], inp.repoOwner) + else: + raise ValueError(invalidRepoOwnerMessage) + if inp.action == updateCommand: + c = repo.get_contributors() + repo.create_secret(inp.secret_names[i], inp.secret_values[i]) + print(f"Secret \"{inp.secret_names[i]}\" updated for {repo.name}") + if not inp.repoOwner == defaultRepoOwnerName: + update_dependabot_secret(inp.token, repo, inp.secret_names[i], inp.secret_values[i], inp.repoOwner) + else: + raise ValueError(invalidRepoOwnerMessage) + if inp.action == deleteCommand: + repo.delete_secret(inp.secret_names[i]) + print(f"Secret \"{inp.secret_names[i]}\" removed from {repo.name}") + if not inp.repoOwner == defaultRepoOwnerName: + delete_dependabot_secret(inp.token, repo, inp.secret_names[i], inp.repoOwner) + else: + raise ValueError(invalidRepoOwnerMessage) + except UnknownObjectException: + print(f"The provided token does not have permission to manage {repo.name}, it is being skipped") + else: + if not inp.interactive or apply_action(repo.name): + try: + if inp.action == createCommand: + add_secret(inp.token, repo, inp.secret_names[i], inp.secret_values[i]) + if inp.action == updateCommand: + c = repo.get_contributors() + repo.create_secret(inp.secret_names[i], inp.secret_values[i]) + print(f"Secret \"{inp.secret_names[i]}\" updated for {repo.name}") + if inp.action == deleteCommand: + repo.delete_secret(inp.secret_names[i]) + print(f"Secret \"{inp.secret_names[i]}\" removed from {repo.name}") + except UnknownObjectException: + print(f"The provided token does not have permission to manage {repo.name}, it is being skipped") \ No newline at end of file