Skip to content

Commit

Permalink
Sync Dependabot Secrets (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
kshitijzutshi222 committed Jun 9, 2023
1 parent 4bb12f9 commit edcb631
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 21 deletions.
10 changes: 10 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
10 changes: 5 additions & 5 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
154 changes: 138 additions & 16 deletions main.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 <PAT>."
invalidTokenMessage = "The token you provided is invalid."
Expand All @@ -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


Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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)

Expand All @@ -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")

0 comments on commit edcb631

Please sign in to comment.