Skip to content

Commit edaac35

Browse files
authored
Merge pull request #398 from GaneshPatil7517/security/harden-contribute-token-handling
security: harden contribute.py with token validation, error sanitization, and retry/backoff (fixes #288)
2 parents 8582910 + 08bd61b commit edaac35

1 file changed

Lines changed: 197 additions & 161 deletions

File tree

contribute.py

Lines changed: 197 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,198 @@
1-
import github
2-
from github import Github
3-
import os,sys,platform,base64,time
4-
5-
# Intializing the Variables
6-
BOT_TOKEN = os.environ.get('CONCORE_BOT_TOKEN', '')
7-
BOT_ACCOUNT = 'concore-bot' #bot account name
8-
REPO_NAME = 'concore-studies' #study repo name
9-
UPSTREAM_ACCOUNT = 'ControlCore-Project' #upstream account name
10-
STUDY_NAME = sys.argv[1]
11-
STUDY_NAME_PATH = sys.argv[2]
12-
AUTHOR_NAME = sys.argv[3]
13-
BRANCH_NAME = sys.argv[4]
14-
PR_TITLE = sys.argv[5]
15-
PR_BODY = sys.argv[6]
16-
17-
# Defining Functions
18-
def checkInputValidity():
19-
if not AUTHOR_NAME or not STUDY_NAME or not STUDY_NAME_PATH:
20-
print("Please Provide necessary Inputs")
21-
exit(1)
22-
if not os.path.isdir(STUDY_NAME_PATH):
23-
print("Directory doesnot Exists.Invalid Path")
24-
exit(1)
25-
26-
def printPR(pr):
27-
print(f'Check your example here https://github.com/{UPSTREAM_ACCOUNT}/{REPO_NAME}/pulls/{pr.number}',end="")
28-
29-
def anyOpenPR(upstream_repo):
30-
try:
31-
prs = upstream_repo.get_pulls(state='open', head=f'{BOT_ACCOUNT}:{BRANCH_NAME}')
32-
return prs[0] if prs.totalCount > 0 else None
33-
except Exception:
34-
print("Unable to fetch PR status. Try again later.")
35-
exit(1)
36-
37-
def commitAndUpdateRef(repo,tree_content,commit,branch):
38-
try:
39-
new_tree = repo.create_git_tree(tree=tree_content,base_tree=commit.commit.tree)
40-
new_commit = repo.create_git_commit(f"Committing Study Named {STUDY_NAME}",new_tree,[commit.commit])
41-
if len(repo.compare(base=commit.commit.sha,head=new_commit.sha).files) == 0:
42-
print("Your don't have any new changes.May be your example is already accepted.If this is not the case try with different fields.")
43-
exit(1)
44-
ref = repo.get_git_ref("heads/"+branch.name)
45-
ref.edit(new_commit.sha,True)
46-
except Exception as e:
47-
print("failed to Upload your example.Please try after some time.",end="")
48-
exit(1)
49-
50-
51-
def appendBlobInTree(repo,content,file_path,tree_content):
52-
blob = repo.create_git_blob(content,'utf-8')
53-
tree_content.append( github.InputGitTreeElement(path=file_path,mode="100644",type="blob",sha=blob.sha))
54-
55-
56-
def runWorkflow(repo,upstream_repo):
57-
openPR = anyOpenPR(upstream_repo)
58-
if not openPR:
59-
try:
60-
repo.get_workflow("pull_request.yml").create_dispatch(
61-
ref=BRANCH_NAME,
62-
inputs={'title': f"[BOT]: {PR_TITLE}", 'body': PR_BODY, 'upstreamRepo': UPSTREAM_ACCOUNT, 'botRepo': BOT_ACCOUNT, 'repo': REPO_NAME}
63-
)
64-
printPRStatus(upstream_repo)
65-
except Exception as e:
66-
print(f"Error triggering workflow. Try again later.\n ERROR: {e}")
67-
exit(1)
68-
else:
69-
print(f"Successfully uploaded. Waiting for approval: https://github.com/{UPSTREAM_ACCOUNT}/{REPO_NAME}/pull/{openPR.number}")
70-
71-
def printPRStatus(upstream_repo):
72-
attempts = 5
73-
delay = 2
74-
for i in range(attempts):
75-
print(f"Attempt: {i}")
76-
try:
77-
latest_pr = upstream_repo.get_pulls(state='open', sort='created', direction='desc')[0]
78-
print(f"Check your example here: https://github.com/{UPSTREAM_ACCOUNT}/{REPO_NAME}/pull/{latest_pr.number}")
79-
return
80-
except Exception:
81-
time.sleep(delay)
82-
delay *= 2
83-
print("Uploaded successfully, but unable to fetch status.")
84-
85-
86-
def isImageFile(filename):
87-
image_extensions = ['.jpeg', '.jpg', '.png','.gif']
88-
return any(filename.endswith(ext) for ext in image_extensions)
89-
90-
def remove_prefix(text, prefix):
91-
if text.startswith(prefix):
92-
return text[len(prefix):]
93-
return text
94-
95-
96-
# Decode Github Token
97-
def decode_token(encoded_token):
98-
decoded_bytes = encoded_token.encode("ascii")
99-
convertedbytes = base64.b64decode(decoded_bytes)
100-
decoded_token = convertedbytes.decode("ascii")
101-
return decoded_token
102-
103-
104-
# check if directory path is Valid
105-
checkInputValidity()
106-
107-
108-
# Authenticating Github with Access token
109-
try:
110-
BRANCH_NAME = AUTHOR_NAME.replace(" ", "_") + "_" + STUDY_NAME if BRANCH_NAME == "#" else BRANCH_NAME.replace(" ", "_")
111-
PR_TITLE = f"Contributing Study {STUDY_NAME} by {AUTHOR_NAME}" if PR_TITLE == "#" else PR_TITLE
112-
PR_BODY = f"Study Name: {STUDY_NAME}\nAuthor Name: {AUTHOR_NAME}" if PR_BODY == "#" else PR_BODY
113-
DIR_PATH = STUDY_NAME
114-
DIR_PATH = DIR_PATH.replace(" ","_")
115-
g = Github(BOT_TOKEN)
116-
repo = g.get_user(BOT_ACCOUNT).get_repo(REPO_NAME)
117-
upstream_repo = g.get_repo(f'{UPSTREAM_ACCOUNT}/{REPO_NAME}') #controlcore-Project/concore-studies
118-
base_ref = upstream_repo.get_branch(repo.default_branch)
119-
120-
try:
121-
repo.get_branch(BRANCH_NAME)
122-
is_present = True
123-
except github.GithubException:
124-
print(f"No Branch is available with the name {BRANCH_NAME}")
125-
is_present = False
126-
except Exception as e:
127-
print("Authentication failed", end="")
128-
exit(1)
129-
130-
131-
try:
132-
if not is_present:
133-
repo.create_git_ref(f"refs/heads/{BRANCH_NAME}", base_ref.commit.sha)
134-
branch = repo.get_branch(BRANCH_NAME)
135-
except Exception:
136-
print("Unable to create study. Try again later.")
137-
exit(1)
138-
139-
140-
tree_content = []
141-
142-
try:
143-
for root, dirs, files in os.walk(STUDY_NAME_PATH):
144-
files = [f for f in files if not f[0] == '.']
145-
for filename in files:
146-
path = f"{root}/{filename}"
147-
if isImageFile(filename):
148-
with open(file=path, mode='rb') as file:
149-
image = file.read()
150-
content = base64.b64encode(image).decode('utf-8')
151-
else:
152-
with open(file=path, mode='r') as file:
153-
content = file.read()
154-
file_path = f'{DIR_PATH+remove_prefix(path,STUDY_NAME_PATH)}'
155-
if(platform.uname()[0]=='Windows'): file_path=file_path.replace("\\","/")
156-
appendBlobInTree(repo,content,file_path,tree_content)
157-
commitAndUpdateRef(repo,tree_content,base_ref.commit,branch)
158-
runWorkflow(repo,upstream_repo)
159-
except Exception as e:
160-
print(e)
161-
print("Some error Occured.Please try again after some time.",end="")
1+
import github
2+
from github import Github
3+
import os,sys,platform,base64,time,re
4+
5+
# Initializing the Variables
6+
BOT_TOKEN = os.environ.get('CONCORE_BOT_TOKEN', '')
7+
8+
# Fail fast if token is missing
9+
if not BOT_TOKEN:
10+
print("Error: CONCORE_BOT_TOKEN environment variable is not set.")
11+
sys.exit(1)
12+
13+
# Token format validation
14+
token_pattern = r"^((ghp_|github_pat_|ghs_)[A-Za-z0-9_]{20,}|[0-9a-fA-F]{40})$"
15+
if not re.match(token_pattern, BOT_TOKEN):
16+
print("Error: Invalid GitHub token format.")
17+
sys.exit(1)
18+
BOT_ACCOUNT = 'concore-bot' #bot account name
19+
REPO_NAME = 'concore-studies' #study repo name
20+
UPSTREAM_ACCOUNT = 'ControlCore-Project' #upstream account name
21+
STUDY_NAME = sys.argv[1]
22+
STUDY_NAME_PATH = sys.argv[2]
23+
AUTHOR_NAME = sys.argv[3]
24+
BRANCH_NAME = sys.argv[4]
25+
PR_TITLE = sys.argv[5]
26+
PR_BODY = sys.argv[6]
27+
28+
# Defining Functions
29+
def checkInputValidity():
30+
if not AUTHOR_NAME or not STUDY_NAME or not STUDY_NAME_PATH:
31+
print("Please Provide necessary Inputs")
32+
exit(1)
33+
if not os.path.isdir(STUDY_NAME_PATH):
34+
print("Directory does not Exists.Invalid Path")
35+
exit(1)
36+
37+
# Retry + backoff wrapper for PyGithub operations
38+
def with_retry(operation, retries=3):
39+
"""Retry wrapper for PyGithub operations with exponential backoff."""
40+
for attempt in range(retries):
41+
try:
42+
return operation()
43+
except github.GithubException as e:
44+
if (e.status == 429 or e.status >= 500) and attempt < retries - 1:
45+
wait_time = 2 ** attempt
46+
time.sleep(wait_time)
47+
continue
48+
raise
49+
print("Error: GitHub API request failed after retries.")
50+
sys.exit(1)
51+
52+
# Correct PR URL (singular 'pull' not 'pulls')
53+
def printPR(pr):
54+
print(f'Check your example here https://github.com/{UPSTREAM_ACCOUNT}/{REPO_NAME}/pull/{pr.number}',end="")
55+
56+
def anyOpenPR(upstream_repo):
57+
try:
58+
prs = upstream_repo.get_pulls(state='open', head=f'{BOT_ACCOUNT}:{BRANCH_NAME}')
59+
return prs[0] if prs.totalCount > 0 else None
60+
except github.GithubException as e:
61+
if e.status == 429 or e.status >= 500:
62+
print("GitHub API rate limit or server error while fetching PR status.")
63+
else:
64+
print("Unable to fetch PR status. Try again later.")
65+
exit(1)
66+
except Exception:
67+
print("Unable to fetch PR status. Try again later.")
68+
exit(1)
69+
70+
def commitAndUpdateRef(repo,tree_content,commit,branch):
71+
try:
72+
new_tree = repo.create_git_tree(tree=tree_content,base_tree=commit.commit.tree)
73+
new_commit = repo.create_git_commit(f"Committing Study Named {STUDY_NAME}",new_tree,[commit.commit])
74+
if len(repo.compare(base=commit.commit.sha,head=new_commit.sha).files) == 0:
75+
print("Your don't have any new changes.May be your example is already accepted.If this is not the case try with different fields.")
76+
exit(1)
77+
ref = repo.get_git_ref("heads/"+branch.name)
78+
ref.edit(new_commit.sha,True)
79+
except github.GithubException as e:
80+
print(f"GitHub API error: {e.status}")
81+
exit(1)
82+
except Exception:
83+
print("Failed to upload your example. Please try after some time.",end="")
84+
exit(1)
85+
86+
87+
def appendBlobInTree(repo,content,file_path,tree_content):
88+
blob = repo.create_git_blob(content,'utf-8')
89+
tree_content.append( github.InputGitTreeElement(path=file_path,mode="100644",type="blob",sha=blob.sha))
90+
91+
92+
def runWorkflow(repo,upstream_repo):
93+
openPR = anyOpenPR(upstream_repo)
94+
if not openPR:
95+
try:
96+
repo.get_workflow("pull_request.yml").create_dispatch(
97+
ref=BRANCH_NAME,
98+
inputs={'title': f"[BOT]: {PR_TITLE}", 'body': PR_BODY, 'upstreamRepo': UPSTREAM_ACCOUNT, 'botRepo': BOT_ACCOUNT, 'repo': REPO_NAME}
99+
)
100+
printPRStatus(upstream_repo)
101+
except github.GithubException as e:
102+
print(f"GitHub API error while triggering workflow: {e.status}")
103+
exit(1)
104+
except Exception:
105+
print("Error triggering workflow. Try again later.")
106+
exit(1)
107+
else:
108+
print(f"Successfully uploaded. Waiting for approval: https://github.com/{UPSTREAM_ACCOUNT}/{REPO_NAME}/pull/{openPR.number}")
109+
110+
def printPRStatus(upstream_repo):
111+
attempts = 5
112+
delay = 2
113+
for i in range(attempts):
114+
print(f"Attempt: {i}")
115+
try:
116+
latest_pr = upstream_repo.get_pulls(state='open', sort='created', direction='desc')[0]
117+
print(f"Check your example here: https://github.com/{UPSTREAM_ACCOUNT}/{REPO_NAME}/pull/{latest_pr.number}")
118+
return
119+
except Exception:
120+
time.sleep(delay)
121+
delay *= 2
122+
print("Uploaded successfully, but unable to fetch status.")
123+
124+
125+
def isImageFile(filename):
126+
image_extensions = ['.jpeg', '.jpg', '.png','.gif']
127+
return any(filename.endswith(ext) for ext in image_extensions)
128+
129+
def remove_prefix(text, prefix):
130+
if text.startswith(prefix):
131+
return text[len(prefix):]
132+
return text
133+
134+
135+
# check if directory path is Valid
136+
checkInputValidity()
137+
138+
139+
# Authenticating Github with Access token
140+
try:
141+
BRANCH_NAME = AUTHOR_NAME.replace(" ", "_") + "_" + STUDY_NAME if BRANCH_NAME == "#" else BRANCH_NAME.replace(" ", "_")
142+
PR_TITLE = f"Contributing Study {STUDY_NAME} by {AUTHOR_NAME}" if PR_TITLE == "#" else PR_TITLE
143+
PR_BODY = f"Study Name: {STUDY_NAME}\nAuthor Name: {AUTHOR_NAME}" if PR_BODY == "#" else PR_BODY
144+
DIR_PATH = STUDY_NAME
145+
DIR_PATH = DIR_PATH.replace(" ","_")
146+
g = Github(BOT_TOKEN)
147+
repo = g.get_user(BOT_ACCOUNT).get_repo(REPO_NAME)
148+
upstream_repo = g.get_repo(f'{UPSTREAM_ACCOUNT}/{REPO_NAME}') #controlcore-Project/concore-studies
149+
base_ref = upstream_repo.get_branch(repo.default_branch)
150+
151+
try:
152+
repo.get_branch(BRANCH_NAME)
153+
is_present = True
154+
except github.GithubException:
155+
print(f"No Branch is available with the name {BRANCH_NAME}")
156+
is_present = False
157+
except github.GithubException as e:
158+
print(f"GitHub API error during authentication: {e.status}")
159+
exit(1)
160+
except Exception:
161+
print("Authentication failed", end="")
162+
exit(1)
163+
164+
165+
try:
166+
if not is_present:
167+
repo.create_git_ref(f"refs/heads/{BRANCH_NAME}", base_ref.commit.sha)
168+
branch = repo.get_branch(BRANCH_NAME)
169+
except Exception:
170+
print("Unable to create study. Try again later.")
171+
exit(1)
172+
173+
174+
tree_content = []
175+
176+
try:
177+
for root, dirs, files in os.walk(STUDY_NAME_PATH):
178+
files = [f for f in files if not f[0] == '.']
179+
for filename in files:
180+
path = f"{root}/{filename}"
181+
if isImageFile(filename):
182+
with open(file=path, mode='rb') as file:
183+
image = file.read()
184+
content = base64.b64encode(image).decode('utf-8')
185+
else:
186+
with open(file=path, mode='r') as file:
187+
content = file.read()
188+
file_path = f'{DIR_PATH+remove_prefix(path,STUDY_NAME_PATH)}'
189+
if(platform.uname()[0]=='Windows'): file_path=file_path.replace("\\","/")
190+
appendBlobInTree(repo,content,file_path,tree_content)
191+
commitAndUpdateRef(repo,tree_content,base_ref.commit,branch)
192+
runWorkflow(repo,upstream_repo)
193+
except github.GithubException as e:
194+
print(f"GitHub API error: {e.status}")
195+
exit(1)
196+
except Exception:
197+
print("Some error occurred. Please try again after some time.",end="")
162198
exit(1)

0 commit comments

Comments
 (0)