From 1008d69c62ef3fbbdcc630358463f677f3b52af8 Mon Sep 17 00:00:00 2001 From: Tim Kelly Date: Mon, 20 Nov 2023 15:51:40 -0600 Subject: [PATCH] first commit --- CONTRIBUTING.md | 27 ++++++ Codeowners | 7 ++ README.md | 43 +++++++++- ghas-scan.py | 209 +++++++++++++++++++++++++++++++++++++++++++++++ output.json | 1 + requirements.txt | 1 + 6 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 Codeowners create mode 100644 ghas-scan.py create mode 100644 output.json create mode 100644 requirements.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0c505de --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing to This Project + +First off, thank you for considering contributing to this project. It's people like you that make this project such a great tool. + +## How Can I Contribute? + +### Reporting Bugs + +- **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/austimkelly/ghas-utils/issues). +- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/austimkelly/ghas-utils/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. + +### Suggesting Enhancements + +If you have a suggestion that is not a bug and may make this project better, you can use the issue tracker to submit your suggestion. Include a clear title and description in your issue. + +### Pull Requests + +- Fork the repository and create your branch from `main`. +- Issue that pull request! + +## Code of Conduct + +This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. + +## Thank You! + +Your contributions to the community are greatly appreciated. Every little bit helps and credit will always be given. \ No newline at end of file diff --git a/Codeowners b/Codeowners new file mode 100644 index 0000000..02678bc --- /dev/null +++ b/Codeowners @@ -0,0 +1,7 @@ +# This is a comment. +# Each line is a file pattern followed by one or more owners. + +# Please note that the CODEOWNERS file needs to be in the repository's root, docs/, or .github/ directory. + +# These owners will be the default owners for everything in the repo. +* @austimkelly \ No newline at end of file diff --git a/README.md b/README.md index c89e636..59cbff0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,41 @@ -# ghas-utils -Utilities for getting insights from Github Advanced Security +# GHAS Scan + +This is a Python script that interacts with the GitHub API to fetch repository details and code scanning analysis information. +Make sure the repository exists and your GitHub token has the necessary permissions to access it. +## Prerequisites + +- Python 3.6 or higher +- `requests` library + +## Installation + +1. Clone this repository: + ```bash + git clone git@github.com:austimkelly/ghas-utils.git + ``` +2. Navigate to the cloned repository: + ```bash + cd ghas-utils + ``` +3. Install the required Python libraries: + ```bash + pip3 install requests + ``` + or + + ```bash + pip3 install -r requests.txt + ``` + +## Usage + +1. Open `ghas-scan.py` in your favorite text editor. +2. Replace `access_token` variable value with your GitHub personal access token. +3. Replace `owner_type` variabe value with `user` or `org`. +4. Replace `owner_name` variable value with the corresponding user or org name. +5. Run the script: + ```bash + python3 ghas-scan.py + ``` + +Output is written to `github_data.json` at the repository root. diff --git a/ghas-scan.py b/ghas-scan.py new file mode 100644 index 0000000..637c6d1 --- /dev/null +++ b/ghas-scan.py @@ -0,0 +1,209 @@ +import csv +import requests +import json + +# https://docs.github.com/en/rest/dependabot/alerts?apiVersion=2022-11-28#list-dependabot-alerts-for-a-repository +import requests + +def get_dependabot_alerts(owner, repo_name, headers): + dependabot_url = f'https://api.github.com/repos/{owner}/{repo_name}/dependabot/alerts' + dependabot_alerts = requests.get(dependabot_url, headers=headers) + + # Check if Dependabot alerts are available + if dependabot_alerts.status_code == 200: + dependabot_alerts_data = dependabot_alerts.json() + + # Check if Dependabot is enabled + dependabot_enabled = True if dependabot_alerts_data else False + + # Filter open alerts + open_alerts = [alert for alert in dependabot_alerts_data if alert['state'] == 'open'] + + # Count the number of open alerts + open_alerts_count = len(open_alerts) + + # Initialize severity counts + num_critical_dep_alerts = 0 + num_high_dep_alerts = 0 + num_medium_dep_alerts = 0 + num_low_dep_alerts = 0 + + # Categorize open alerts based on severity + for alert in open_alerts: + severity = alert['security_advisory']['severity'] + if severity == 'critical': + num_critical_dep_alerts += 1 + elif severity == 'high': + num_high_dep_alerts += 1 + elif severity == 'medium': + num_medium_dep_alerts += 1 + elif severity == 'low': + num_low_dep_alerts += 1 + + else: + dependabot_enabled = False + open_alerts_count = 0 + num_critical_dep_alerts = 0 + num_high_dep_alerts = 0 + num_medium_dep_alerts = 0 + num_low_dep_alerts = 0 + + return ( + dependabot_enabled, + open_alerts_count, + num_critical_dep_alerts, + num_high_dep_alerts, + num_medium_dep_alerts, + num_low_dep_alerts + ) + +def get_tool_name(owner, repo, headers): + url = "https://api.github.com/repos/" + owner + "/" + repo + "/code-scanning/analyses" + + try: + response = requests.get(url, headers=headers) + if response.status_code == 404: + return "None" + response.raise_for_status() # Raises a HTTPError if the status is 4xx, 5xx + data = response.json() + + # Specify the filename + filename = "output.json" + + # Open the file in write mode ('w') + with open(filename, 'w') as file: + # Use json.dump to write the data to the file + json.dump(data, file) + + if data: + return data[0]['tool']['name'] + else: + return "No data received from the server" + except requests.exceptions.HTTPError as http_err: + return f"HTTPError: {http_err}" + except Exception as err: + return f"Error occurred: {err}" + +def get_codeowners(owner, repo_name, headers): + codeowners_url = f'https://api.github.com/repos/{owner}/{repo_name}/community/codeowners' + codeowners_response = requests.get(codeowners_url, headers=headers) + if codeowners_response.status_code == 200: + codeowners = codeowners_response.text + else: + codeowners = "Not found" + return codeowners + +def get_repo_details(owner, repo_name): + # Construct the repository URL using the owner and repo_name variables + repo_url = f'https://api.github.com/repos/{owner}/{repo_name}' + + # Send a GET request to the repo_url and retrieve the repository information + repo_info = requests.get(repo_url, headers=headers).json() + + # Additional API calls for more details + # You may need to handle pagination for larger repositories + + # Get CODEOWNERS file + codeowners =get_codeowners(owner, repo_name, headers) + + # Last commit date + commits_url = f'https://api.github.com/repos/{owner}/{repo_name}/commits?per_page=1' + last_commit_date = requests.get(commits_url, headers=headers).json()[0]['commit']['author']['date'] + + # First commit date + first_commit_url = f'https://api.github.com/repos/{owner}/{repo_name}/commits?per_page=1' + first_commit_date = requests.get(first_commit_url, headers=headers).json()[-1]['commit']['author']['date'] + + # Is a fork + is_fork = repo_info['fork'] + + # Public or private + is_private = repo_info['private'] + + # Is archived + is_archived = repo_info['archived'] + + # Check if secrets scanning is enabled + secrets_scanning_url = f'https://api.github.com/repos/{owner}/{repo_name}/secret-scanning/alerts' + secrets_scanning_enabled = len(requests.get(secrets_scanning_url, headers=headers).json()) > 0 + + # Get names of code scanners + # https://docs.github.com/en/rest/code-scanning/code-scanning?apiVersion=2022-11-28#list-code-scanning-analyses-for-a-repository + code_scanners_enabled = get_tool_name(owner, repo_name, headers ) + + # Check the number of Dependabot alerts + dependabot_enabled, open_alerts_count, num_critical_dep_alerts, num_high_dep_alerts, num_medium_dep_alerts, num_low_dep_alerts = get_dependabot_alerts(owner, repo_name, headers) + + # Other details can be fetched similarly + + return { + 'repo_name': repo_info['name'], + 'codeowners': codeowners, + 'last_commit_date': last_commit_date, + 'first_commit_date': first_commit_date, + 'is_fork': is_fork, + 'is_private': is_private, + 'is_archived': is_archived, + 'secrets_scanning_enabled': secrets_scanning_enabled, + 'code_scanners_enabled': code_scanners_enabled, + 'dependabot_enabled': dependabot_enabled, + 'dependabot_open_alerts_count': open_alerts_count, + 'num_critical_dep_alerts': num_critical_dep_alerts, + 'num_high_dep_alerts': num_high_dep_alerts, + 'num_medium_dep_alerts': num_medium_dep_alerts, + 'num_low_dep_alerts': num_low_dep_alerts + # Add other details here + } + +def get_repos(owner): + if owner_type == 'user': + repos_url = f'https://api.github.com/users/{owner}/repos' + elif owner_type == 'org': + repos_url = f'https://api.github.com/orgs/{owner}/repos' + else: + raise ValueError("Invalid owner type. Use 'user' or 'org'.") + + response = requests.get(repos_url, headers=headers) + if response.status_code == 200: + repos = response.json() + return repos + else: + raise Exception(f"Failed to fetch repositories. Status code: {response.status_code}, Response: {response.text}") + +# Set the GitHub owner type, owner name, and personal access token +owner_type = 'user' # Change to 'org' if needed +owner_name = 'austimkelly' +access_token = 'CHANGEME' + +# Set up headers with the access token +headers = {'Authorization': f'token {access_token}'} + +# Get list of repositories for the user or organization +repos = get_repos(owner_name) + +# Write data to CSV +csv_filename = 'github_data.csv' +with open(csv_filename, 'w', newline='') as csvfile: + fieldnames = ['repo_name', + 'codeowners', + 'last_commit_date', + 'first_commit_date', + 'is_fork', + 'is_private', + 'is_archived', + 'secrets_scanning_enabled', + 'code_scanners_enabled', + 'dependabot_enabled', + 'dependabot_open_alerts_count', + 'num_critical_dep_alerts', + 'num_high_dep_alerts', + 'num_medium_dep_alerts', + 'num_low_dep_alerts',] + + # Add other fieldnames here + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + + for repo in repos: + repo_details = get_repo_details(owner_name, repo['name']) + writer.writerow(repo_details) diff --git a/output.json b/output.json new file mode 100644 index 0000000..cd78b77 --- /dev/null +++ b/output.json @@ -0,0 +1 @@ +[{"ref": "refs/heads/main", "commit_sha": "588ef7b372c84e2c504aa2557224d98b90618756", "analysis_key": ".github/workflows/codeql.yml:analyze", "environment": "{\"language\":\"python\"}", "category": "/language:python", "error": "", "created_at": "2023-11-18T17:32:41Z", "results_count": 0, "rules_count": 36, "id": 142162467, "url": "https://api.github.com/repos/austimkelly/rekon/code-scanning/analyses/142162467", "sarif_id": "7a0572fa-8638-11ee-97f0-64f950e652f6", "tool": {"name": "CodeQL", "guid": null, "version": "2.15.2"}, "deletable": true, "warning": ""}] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4f5b899 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests==2.26.0 \ No newline at end of file