Skip to content

Commit fb5b10e

Browse files
security: harden contribute.py with token validation, safe error handling, and retry logic (fixes #288)
1 parent 0467ce5 commit fb5b10e

1 file changed

Lines changed: 213 additions & 161 deletions

File tree

contribute.py

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

0 commit comments

Comments
 (0)