Skip to content

Commit 008c673

Browse files
Add log level support
1 parent 6af7a8d commit 008c673

File tree

6 files changed

+76
-53
lines changed

6 files changed

+76
-53
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Workflow auditing tools to identify security issues in GitHub workflows
88
# Usage
99

1010
```
11-
usage: main.py [-h] [--type {repo,org,user}] input
11+
usage: main.py [-h] [--type {repo,org,user}] [--log-level {debug,info,warning,error,critical}] input
1212
1313
Identify vulnerabilities in GitHub Actions workflow
1414
@@ -19,6 +19,8 @@ optional arguments:
1919
-h, --help show this help message and exit
2020
--type {repo,org,user}
2121
Type of entity that is being scanned.
22+
--log-level {debug,info,warning,error,critical}
23+
Log level for output
2224
```
2325

2426
Example:

action_auditor.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
from github_wrapper import GHWrapper
2-
from lib.logger import AuditLogger
31
from pathlib import Path
42
import re
53

6-
gh = GHWrapper()
7-
84
def read_actions_file():
95
array_of_usernames = []
106
with open('actions.txt','r') as lines:
@@ -16,17 +12,22 @@ def read_actions_file():
1612
array_of_usernames.append(username)
1713
return array_of_usernames
1814

19-
def check_usernames(username_list):
20-
for username in username_list:
21-
renamed_or_not = gh.stale_checker(username=username)
22-
if not renamed_or_not:
23-
AuditLogger.warning(f"Security Issue: Supply chain. {username} was renamed but used in workflows. Signup the username at https://github.com to make sure.")
15+
class ActionAuditor:
16+
def __init__(self, gh_wrapper, logger):
17+
self.gh = gh_wrapper
18+
self.logger = logger
19+
20+
def check_usernames(self, username_list):
21+
for username in username_list:
22+
renamed_or_not = self.gh.stale_checker(username=username)
23+
if not renamed_or_not:
24+
self.logger.warning(f"Security Issue: Supply chain. {username} was renamed but used in workflows. Signup the username at https://github.com to make sure.")
2425

25-
def action_audit():
26-
if Path('actions.txt').exists():
27-
usernames = read_actions_file()
28-
check_usernames(usernames)
29-
Path('actions.txt').unlink()
30-
else:
31-
AuditLogger.info("No actions.txt file to scan. Supply chain scan complete.")
26+
def action_audit(self):
27+
if Path('actions.txt').exists():
28+
usernames = read_actions_file()
29+
self.check_usernames(usernames)
30+
Path('actions.txt').unlink()
31+
else:
32+
self.logger.info("No actions.txt file to scan. Supply chain scan complete.")
3233

auditor.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from workflow import WorkflowParser, WorkflowVulnAudit
2-
from lib.logger import AuditLogger
32

43
vuln_analyzer = WorkflowVulnAudit()
54

@@ -15,6 +14,7 @@ def risky_trigger_analysis(identified_triggers):
1514
"""
1615
Input:
1716
content - YAML content read from the workflow files.
17+
logger - configured logger
1818
Output:
1919
scan result (if any) in scan.log file.
2020
Summary:
@@ -24,7 +24,7 @@ def risky_trigger_analysis(identified_triggers):
2424
jobs and steps. It then checks the identified key-value pairs
2525
against known risks through WorkflowParser and WorkflowVulnAudit.
2626
"""
27-
def content_analyzer(content):
27+
def content_analyzer(content, logger):
2828
risky_triggers = []
2929
all_actions = []
3030
commands = []
@@ -38,7 +38,7 @@ def content_analyzer(content):
3838

3939
counter = 1 # Counter used to identify which line of code is vulnerable.
4040
if secrets:
41-
AuditLogger.info(f">>> Secrets used in workflow: {','.join(secrets)}")
41+
logger.info(f">>> Secrets used in workflow: {','.join(secrets)}")
4242

4343
# Retrieve and store all needed information for a workflow run for analysis.
4444
if all_jobs:
@@ -49,7 +49,7 @@ def content_analyzer(content):
4949
try:
5050
environs.update(all_jobs[job].get('env',{}))
5151
except:
52-
AuditLogger.info(">> Environ variable is malformed")
52+
logger.info(">> Environ variable is malformed")
5353
for step_number,step in enumerate(steps):
5454
actions, run_command, with_input, step_environ = workflow_client.analyze_step(step)
5555
if actions:
@@ -82,9 +82,9 @@ def content_analyzer(content):
8282
if environ_var_value:
8383
risky_env = vuln_analyzer.risky_command(command_string=environ_var_value)
8484
if risky_env and list(risky_env.keys())[0] != 'environ_regex':
85-
AuditLogger.warning(f">>> Security Issue: RCE detected with {regex} in {step_number}: ENV variable {environ_variable} is called through GitHub context and takes user input {environ_var_value}")
85+
logger.warning(f">>> Security Issue: RCE detected with {regex} in {step_number}: ENV variable {environ_variable} is called through GitHub context and takes user input {environ_var_value}")
8686
else:
87-
AuditLogger.warning(f">>> Security Issue: RCE detected with {regex} in {step_number}: Usage of {','.join(matched_strings)} found.")
87+
logger.warning(f">>> Security Issue: RCE detected with {regex} in {step_number}: Usage of {','.join(matched_strings)} found.")
8888

8989
# Some actions combined with triggers can be bad. Check for those cases.
9090
action_storage = open('actions.txt','a+')
@@ -100,7 +100,7 @@ def content_analyzer(content):
100100
risky_commits = vuln_analyzer.risky_commit(referenced=ref_value)
101101
if risky_commits:
102102
if 'pull_request_target' in risky_triggers:
103-
AuditLogger.warning(f">>> Security Issue: Malicious pull request used in actions/checkout. Vulnerable step: {step_number} ")
103+
logger.warning(f">>> Security Issue: Malicious pull request used in actions/checkout. Vulnerable step: {step_number} ")
104104
action_storage.close()
105105
except Exception as workflow_err:
106-
AuditLogger.info(f">>> Error parsing workflow. Error is {str(workflow_err)}")
106+
logger.info(f">>> Error parsing workflow. Error is {str(workflow_err)}")

github_wrapper.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
import os
22
import sys
33
import requests
4-
from lib.logger import AuditLogger
54

65
from query_data import return_query, validation_query
76

87
"""
98
Input:
109
token - GitHub PAT. Retrieved from environment variable.
10+
logger - Configured logger
1111
1212
Summary:
1313
This wrapper uses GitHub's GraphQL API and repository(ies)
1414
for the provided scan target. In addition, it is also used
1515
at the end of the workflow for stale account checks.
1616
"""
1717
class GHWrapper():
18-
def __init__(self):
18+
def __init__(self, logger):
1919
self.token = os.environ.get('PAT',None)
20+
self.logger = logger
2021
if self.token is None:
21-
AuditLogger.warning("No GitHub token provided in the PAT env variable. Exiting.")
22+
self.logger.warning("No GitHub token provided in the PAT env variable. Exiting.")
2223
sys.exit()
2324
if not self.validate_token():
24-
AuditLogger.warning("GitHub token provided in the PAT env variable is invalid. Exiting.")
25+
self.logger.warning("GitHub token provided in the PAT env variable is invalid. Exiting.")
2526
sys.exit()
2627

2728
def validate_token(self):
@@ -45,7 +46,7 @@ def call_graphql(self, query):
4546
return query_request.json()
4647
else:
4748
message = query_request.text
48-
AuditLogger.error(f"GitHub GraphQL Query failed: {message}")
49+
logger.error(f"GitHub GraphQL Query failed: {message}")
4950
sys.exit(1)
5051

5152
def repo_node_parser(self,repo_node):
@@ -74,11 +75,11 @@ def get_single_repo(self, repo_name):
7475
if repo_workflows: # this repo has workflows
7576
repos_all[repo_name] = repo_workflows
7677
else:
77-
AuditLogger.debug(f"Repo {repo_name} has no workflow.")
78+
self.logger.debug(f"Repo {repo_name} has no workflow.")
7879
return repos_all
7980

8081
def get_multiple_repos(self,target_name,target_type='org'):
81-
AuditLogger.info(f"---- Getting repos for {target_name}----")
82+
self.logger.info(f"---- Getting repos for {target_name}----")
8283
repos_all = {}
8384
query_type = {'org':'organization','user':'user','repo':'repository'}
8485
try:
@@ -98,16 +99,16 @@ def get_multiple_repos(self,target_name,target_type='org'):
9899
repos_all[repo_name] = repo_workflows
99100
count += 1
100101
else:
101-
AuditLogger.debug(f"Repo {repo_name} has no workflow.")
102+
self.logger.debug(f"Repo {repo_name} has no workflow.")
102103
has_more = repos['data'][query_type[target_type]]['repositories']['pageInfo']['hasNextPage']
103104
next_cursor = repos['data'][query_type[target_type]]['repositories']['pageInfo']['endCursor']
104105
if has_more:
105-
AuditLogger.info("> Retrieve next batch of 100 repos.")
106+
logger.info("> Retrieve next batch of 100 repos.")
106107
else:
107-
AuditLogger.error(f"GraphQL response had error.")
108+
self.logger.error(f"GraphQL response had error.")
108109
sys.exit(1)
109110
except Exception as repo_err:
110-
AuditLogger.debug(f"Error parsing data. Message: {str(repo_err)}")
111+
self.logger.debug(f"Error parsing data. Message: {str(repo_err)}")
111112
return count, repos_all
112113

113114
def stale_checker(self,username):

lib/logger.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
11
import logging
22

3-
def build_logger():
3+
def parse_log_level_input(input):
4+
if input == 'debug':
5+
level = logging.DEBUG
6+
elif input == 'info':
7+
level = logging.INFO
8+
elif input == 'warning':
9+
level = logging.WARNING
10+
elif input == 'error':
11+
level = logging.ERROR
12+
elif input == 'critical':
13+
level = logging.CRITICAL
14+
else:
15+
input = logging.INFO
16+
17+
return level
18+
19+
def build_logger(log_level='info'):
420
log_format = logging.Formatter('%(levelname)s: %(message)s')
521
logger = logging.getLogger('Audit Log')
6-
logger.setLevel(logging.INFO)
22+
log_level = parse_log_level_input(log_level)
23+
logger.setLevel(log_level)
724

825
channel = logging.StreamHandler()
926
channel.setFormatter(log_format)
1027

1128
log_file = logging.FileHandler('scan.log')
12-
log_file.setLevel(logging.INFO)
1329

1430
logger.addHandler(channel)
1531
logger.addHandler(log_file)
1632
return logger
1733

18-
AuditLogger = build_logger()

main.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
# Local imports
44
from auditor import content_analyzer
5-
from action_auditor import action_audit
5+
from action_auditor import ActionAuditor
66
from github_wrapper import GHWrapper
7-
from lib.logger import AuditLogger
7+
from lib.logger import build_logger
88

99

1010
"""
@@ -18,39 +18,43 @@
1818
function will call content_analyzer to audit the workflow
1919
for any potential vulnerabilities.
2020
"""
21-
def repo_analysis(repo_workflow):
21+
def repo_analysis(repo_workflow, logger):
2222
for workflow in repo_workflow:
2323
workflow_name = workflow['name']
2424
workflow_content = workflow['content']
25-
AuditLogger.info(f">> Scanning: {workflow_name}")
26-
content_analyzer(content=workflow_content) # will print out security issues
25+
logger.info(f">> Scanning: {workflow_name}")
26+
content_analyzer(content=workflow_content, logger=logger) # will print out security issues
2727

2828
def main():
29-
# Supporting user provided arguments: type, and scan target.
29+
# Supporting user provided arguments: type, log level, and scan target.
3030
parser = argparse.ArgumentParser(description='Identify vulnerabilities in GitHub Actions workflow')
3131
parser.add_argument('--type',choices=['repo','org','user'],
3232
help='Type of entity that is being scanned.')
33+
parser.add_argument('--log-level',choices=['debug','info','warning','error','critical'], default='info')
3334
parser.add_argument('input',help='Org, user or repo name (owner/name)')
3435
args = parser.parse_args()
3536

36-
gh = GHWrapper()
37-
3837
target_type = args.type #repo, org, or user
3938
target_input = args.input #can be repo url, or a username for org/user
39+
log_level = args.log_level
40+
41+
logger = build_logger(log_level)
42+
gh = GHWrapper(logger)
4043

4144
if target_type == 'repo':
4245
repos = gh.get_single_repo(repo_name=target_input)
4346
else:
4447
count, repos = gh.get_multiple_repos(target_name=target_input,
4548
target_type=target_type)
46-
AuditLogger.info(f"Metric: Scanning total {count} repos")
49+
logger.info(f"Metric: Scanning total {count} repos")
4750

4851
for repo_dict in repos:
49-
AuditLogger.info(f"> Starting audit of {repo_dict}")
52+
logger.info(f"> Starting audit of {repo_dict}")
5053
repo_workflows = repos[repo_dict]
51-
repo_analysis(repo_workflows)
54+
repo_analysis(repo_workflows, logger)
5255

53-
AuditLogger.info(f"> Checking for supply chain attacks.")
54-
action_audit()
56+
logger.info(f"> Checking for supply chain attacks.")
57+
action_auditor = ActionAuditor(gh, logger)
58+
action_auditor.action_audit()
5559

5660
main()

0 commit comments

Comments
 (0)