You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

132 lines
5.3 KiB

import argparse
import os
import yaml
def lint(filename):
findings = []
with open(filename) as file:
workflow = yaml.load(file, Loader=yaml.FullLoader)
# Check for 'name' key for the workflow.
if "name" not in workflow:
findings.append("- Name key missing for workflow.")
# Check for 'name' value to be capitalized in workflow.
elif not workflow["name"][0].isupper():
findings.append(
f"- Name value for workflow is not capitalized. [{workflow['name']}]"
)
# Loop through jobs in workflow.
if "jobs" in workflow:
jobs = workflow["jobs"]
for job_key in jobs:
job = jobs[job_key]
# Make sure runner is using pinned version.
runner = job["runs-on"]
if "-latest" in runner:
findings.append(
f"- Runner version is set to '{runner}', but needs to be pinned to a version."
)
# Check for 'name' key for job.
if "name" not in job:
findings.append(f"- Name key missing for job key '{job_key}'.")
# Check for 'name' value to be capitallized in job.
elif not job["name"][0].isupper():
findings.append(
f"- Name value of job key '{job_key}' is not capitalized. [{job['name']}]"
)
# If the job has environment variables defined, then make sure they start with an underscore.
if "env" in job:
for k in job["env"].keys():
if k[0] != "_":
findings.append(
f"- Environment variable '{k}' of job key '{job_key}' does not start with an underscore."
)
# Loop through steps in job.
steps = job["steps"]
for i, step in enumerate(steps, start=1):
# Check for 'name' key for step.
if "name" not in step:
findings.append(
f"- Name key missing for step {str(i)} of job key '{job_key}'."
)
# Check for 'name' value to be capitalized in step.
elif not step["name"][0].isupper():
findings.append(
f"- Name value in step {str(i)} of job key '{job_key}' is not capitalized. [{step['name']}]"
)
# If the step has a 'uses' key, check value hash.
if "uses" in step:
try:
_, hash = step["uses"].split("@")
# Check to make sure SHA1 hash is 40 characters.
if len(hash) != 40:
findings.append(
f"- Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not 40 characters)"
)
# Attempts to convert the hash to a integer
# which will succeed if all characters are hexadecimal
try:
int(hash, 16)
except ValueError:
findings.append(
f"- Step {str(i)} of job key '{job_key}' does not have a valid action hash. (not all hexadecimal characters)"
)
except:
findings.append(
f"- Step {str(i)} of job key '{job_key}' does not have a valid action hash. (missing '@' character)"
)
# If the step has a 'run' key and only has one command, check if it's a single line.
if "run" in step:
if step["run"].count('\n') == 1:
findings.append(
f"- Run in step {str(i)} of job key '{job_key}' should be a single line."
)
if len(findings) > 0:
print("#", filename)
for finding in findings:
print(finding)
print()
def main():
# Read arguments from command line.
parser = argparse.ArgumentParser()
parser.add_argument("input", help="file or directory input")
args = parser.parse_args()
# Set up list for files to lint.
input_files = []
# Check if argument is file, then append to input files.
if os.path.isfile(args.input):
input_files.append(args.input)
# Check if argument is directory, then recursively add all *.yml and *.yaml files to input files.
elif os.path.isdir(args.input):
for subdir, dirs, files in os.walk(args.input):
for filename in files:
filepath = subdir + os.sep + filename
if filepath.endswith(".yml") or filepath.endswith(".yaml"):
input_files.append(filepath)
else:
print("File/Directory does not exist, exiting.")
for filename in input_files:
lint(filename)
if __name__ == "__main__":
main()