diff --git a/scripts/dependabot/README.md b/scripts/dependabot/README.md new file mode 100644 index 0000000..0be97c8 --- /dev/null +++ b/scripts/dependabot/README.md @@ -0,0 +1,161 @@ +# **Dependabot and OpsLevel Integration** + +This project provides a Python script that integrates **Dependabot alerts** with **OpsLevel** to automate the tracking and management of vulnerabilities across your repositories. The script fetches alerts from GitHub using the Dependabot API, processes the data, and sends it to OpsLevel via a custom event integration endpoint. + +With this integration, you can also create **custom checks** in OpsLevel to ensure that critical vulnerabilities are addressed promptly. + +--- + +## **Features** +- [List Dependabot alerts for a repository](https://docs.github.com/en/rest/dependabot/alerts?apiVersion=2022-11-28#list-dependabot-alerts-for-a-repository) using the GitHub API. Update the API request as needed to use + - [List Dependabot alerts for an enterprise](https://docs.github.com/en/rest/dependabot/alerts?apiVersion=2022-11-28#list-dependabot-alerts-for-an-enterprise) + - [List Dependabot alerts for an organization](https://docs.github.com/en/rest/dependabot/alerts?apiVersion=2022-11-28#list-dependabot-alerts-for-an-organization) +- Processes the alerts and groups them by severity (e.g., `critical`, `high`, `medium`). +- Sends the processed data to OpsLevel's custom event integration endpoint. +- Supports custom OpsLevel checks to monitor and validate vulnerabilities. + +--- + +## **Prerequisites** +1. **GitHub Personal Access Token**: + - Required to authenticate with the GitHub API. + - Token must have the `security_events` scope for accessing Dependabot alerts. + +2. **OpsLevel Routing ID**: + - A unique identifier for the OpsLevel custom event integration endpoint. + +3. **Python Environment**: + - Python `>=3.7` installed. + - Dependencies installed via `pip` (see [Installation](#installation)). + +--- + +## **Setup** + +### **1. Clone the Repository** +```bash +git clone https://github.com/OpsLevel/community-integrations/dependabot.git +cd dependabot-opslevel-integration +``` +### **2. Create a Configuration File** + +To store your GitHub and OpsLevel credentials securely, create a .env file in the root directory of the project with the following content: +```bash +GITHUB_TOKEN=your_github_personal_access_token +OPSLEVEL_ROUTING_ID=your_opslevel_routing_id +REPO_OWNER=your_repo_owner +REPO_NAME=your_repo_name +``` +- **GITHUB_TOKEN**: Your GitHub Personal Access Token with the security_events scope. +- **OPSLEVEL_ROUTING_ID**: The unique routing ID for your OpsLevel custom event integration. +- **REPO_OWNER**: The owner of the repository (organization or username). +- **REPO_NAME**: The name of the repository. + +### **3. Install Dependencies** +Install the required Python packages: +```bash +pip install -r requirements.txt +``` +### **4. Usage** +Run the Python script to fetch Dependabot alerts and send them to OpsLevel: +```bash +python dependabot_alerts.py +``` +### **5. How It Works** +1. **Fetch Dependabot Alerts**: + - The script queries the GitHub API for Dependabot alerts for the specified repository. + +2. **Process Alerts**: + - Alerts are grouped by severity (critical, high, medium, etc.). + - Each alert includes details like the dependency, vulnerability description, CVEs, and fix availability. + +3. **Send to OpsLevel**: + - The processed data is sent to OpsLevel's custom event integration endpoint. + - Example payload: + ```json + { + "dependabot_alerts": { + "high": { + "open": 2, + "alerts": [ + { + "fix": "No fix available", + "cves": [ + "CVE-2021-23337" + ], + "state": "open", + "dependency": "lodash", + "vulnerability": "Command Injection in lodash" + }, + { + "fix": "No fix available", + "cves": [ + "CVE-2021-23337" + ], + "state": "open", + "dependency": "lodash", + "vulnerability": "Command Injection in lodash" + } + ], + "closed": 0 + }, + "medium": { + "open": 2, + "alerts": [ + { + "fix": "No fix available", + "cves": [ + "CVE-2020-28500" + ], + "state": "open", + "dependency": "lodash", + "vulnerability": "Regular Expression Denial of Service (ReDoS) in lodash" + }, + { + "fix": "No fix available", + "cves": [ + "CVE-2020-28500" + ], + "state": "open", + "dependency": "lodash", + "vulnerability": "Regular Expression Denial of Service (ReDoS) in lodash" + } + ], + "closed": 0 + }, + "critical": { + "open": 1, + "alerts": [ + { + "fix": "No fix available", + "cves": [ + "CVE-2020-6836" + ], + "state": "open", + "dependency": "hot-formula-parser", + "vulnerability": "Command Injection in hot-formula-parser" + } + ], + "closed": 0 + }, + "repository": "dependabot-demo" + } + } + ```~ +## **6. OpsLevel Custom Event Check Setup** +1. **Log in to OpsLevel**. +2. **Create a Custom Event Check**: + - Navigate to **Maturity > Rubric > Add Check**. + - Define a check to query the custom event data for critical vulnerabilities. +3. **Example Check Logic**: + - Component specifier: `.dependabot_alerts | .repository`. + - Success condition: `.dependabot_alerts | select(.repository == $ctx.alias) | .high.open == "0"` + - Result Message: + ```bash + {% if check.passed %} + ### Check passed + {% else %} + ### Check failed + Service **{{ data.dependabot_alerts.repository }}** has **{{ data.dependabot_alerts.high.open }}** unresolved vulnerabilities. + {% endif %} + ``` \ No newline at end of file diff --git a/scripts/dependabot/dependabot_alerts.py b/scripts/dependabot/dependabot_alerts.py new file mode 100644 index 0000000..5128610 --- /dev/null +++ b/scripts/dependabot/dependabot_alerts.py @@ -0,0 +1,102 @@ +import requests +import json +import os +from collections import defaultdict + +# Replace these with your details +GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] +REPO_OWNER = os.environ["REPO_OWNER"] +REPO_NAME = os.environ["REPO_NAME"] +OPSLEVEL_URL = "https://upload.opslevel.com/integrations/custom_event/" +OPSLEVEL_ROUTING_ID = os.environ["OPSLEVEL_ROUTING_ID"] + +# GitHub API URL for Dependabot alerts +URL = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/dependabot/alerts" + +# Headers for GitHub API and OpsLevel +GITHUB_HEADERS = { + "Authorization": f"Bearer {GITHUB_TOKEN}", + "Accept": "application/vnd.github+json", +} + +OPSLEVEL_HEADERS = { + "content-type": "application/json", + "X-OpsLevel-Routing-ID": OPSLEVEL_ROUTING_ID, +} + +def fetch_dependabot_alerts(): + response = requests.get(URL, headers=GITHUB_HEADERS) + if response.status_code != 200: + print(f"Failed to fetch alerts: {response.status_code}") + print(response.json()) + return None + return response.json() + +def organize_alerts_by_severity(alerts): + # Group alerts by severity + severity_grouped = defaultdict(lambda: {"open": 0, "closed": 0, "alerts": []}) + for alert in alerts: + severity = alert['security_advisory']['severity'] + state = alert['state'] + identifiers = [id['value'] for id in alert['security_advisory']['identifiers'] if id['type'] == 'CVE'] + + # Increment open or closed alert count + if state == "open": + severity_grouped[severity]["open"] += 1 + elif state in {"fixed", "dismissed"}: + severity_grouped[severity]["closed"] += 1 + + # Add alert details + severity_grouped[severity]["alerts"].append({ + "dependency": alert['dependency']['package']['name'], + "vulnerability": alert['security_advisory']['summary'], + "cves": identifiers, + "state": state, + "fix": alert.get('fixed_in', 'No fix available'), + }) + return severity_grouped + +def send_to_opslevel(payload): + """ + Send the JSON payload to OpsLevel custom event integration. + """ + try: + response = requests.post(OPSLEVEL_URL, headers=OPSLEVEL_HEADERS, json=payload) + if response.status_code == 202: + print("Payload sent successfully to OpsLevel.") + else: + print(f"Failed to send payload: {response.status_code}") + print(response.text) + except requests.RequestException as e: + print(f"Error sending data to OpsLevel: {e}") + +def save_to_json(data, filename="dependabot_alerts.json"): + with open(filename, "w") as json_file: + json.dump(data, json_file, indent=4) + print(f"Alerts saved to {filename}") + + +def main(): + # Fetch Dependabot alerts + alerts = fetch_dependabot_alerts() + if not alerts: + print("No alerts to process. Exiting.") + return + + # Organize alerts by severity + grouped_alerts = organize_alerts_by_severity(alerts) + + # Prepare the payload + ol_req_payload = { + "dependabot_alerts": { + **grouped_alerts, + "repository": f"{REPO_NAME}" + } + } + + # Send the payload to OpsLevel + send_to_opslevel(ol_req_payload) + save_to_json(ol_req_payload) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/export_users_and_teams/README.md b/scripts/export_users_and_teams/README.md new file mode 100644 index 0000000..60b58cb --- /dev/null +++ b/scripts/export_users_and_teams/README.md @@ -0,0 +1,105 @@ +```markdown +# OpsLevel Teams and Users Exporter + +This Python script retrieves team and user data from OpsLevel using the GraphQL API and exports it to a CSV file. + +## Prerequisites + +* Python 3.6 or later +* `requests` library (`pip install requests`) +* OpsLevel API token (set as environment variable `OPSLEVEL_API_TOKEN`) + +## Setup + +1. **Install Python Dependencies:** + + ```bash + pip install requests + ``` + +2. **Set OpsLevel API Token:** + + Set your OpsLevel API token as an environment variable named `OPSLEVEL_API_TOKEN` and the file path as `OUTPUT_CSV_PATH`. + + * **Linux/macOS:** + + ```bash + export OPSLEVEL_API_TOKEN="your_opslevel_api_token" + export OUTPUT_CSV_PATH="path_to_write_file" + ``` + + * **Windows (Command Prompt):** + + ```bash + set OPSLEVEL_API_TOKEN=your_opslevel_api_token + set OUTPUT_CSV_PATH=path_to_write_file + ``` + + * **Windows (PowerShell):** + + ```powershell + $env:OPSLEVEL_API_TOKEN = "your_opslevel_api_token" + $env:OUTPUT_CSV_PATH = "path_to_write_file" + ``` + + * **Best practice:** For production systems, consider using more secure methods to store and retrieve your API token, such as environment files or secret management tools. + +## Usage + +1. **Run the script:** + + ```bash + python your_script_name.py + ``` + + Replace `your_script_name.py` with the actual name of your Python script. + +2. **Output:** + + The script will create a CSV file named `teams_and_users.csv` in the same directory as the script. This file will contain the exported team and user data. + +## CSV File Structure + +The `teams_and_users.csv` file will have the following columns: + +* `Name` +* `Team Alias` +* `Contact Type` +* `Contact Display Name` +* `Contact Address` +* `User ID` +* `User Name` +* `User Email` +* `Membership Role` + +## Script Explanation + +The script consists of two main functions: + +* **`get_all_teams_and_users(api_token)`:** + * Retrieves team and user data from the OpsLevel GraphQL API. + * Handles pagination to retrieve all data. + * Returns a list of team data dictionaries. +* **`export_teams_and_users_to_csv(teams_data, output_csv_path="teams_and_users.csv")`:** + * Exports the team and user data to a CSV file. + * Handles cases where teams may not have contacts or memberships. + * Writes the data in a structured format. +* **Main execution (`if __name__ == "__main__":`)** + * Retrieves the API token from the environment variables. + * Calls the `get_all_teams_and_users` function to retrieve the data. + * Calls the `export_teams_and_users_to_csv` function to export the data to a CSV file. + +## Error Handling + +The script includes error handling for: + +* Invalid API responses. +* API request errors. +* CSV file writing errors. +* Missing API token environment variable. + +## Notes + +* Ensure that you have the necessary permissions to access the OpsLevel API. +* The CSV file will overwrite any existing file with the same name. +``` \ No newline at end of file diff --git a/scripts/export_users_and_teams/export-teams-users.py b/scripts/export_users_and_teams/export-teams-users.py new file mode 100644 index 0000000..93fb301 --- /dev/null +++ b/scripts/export_users_and_teams/export-teams-users.py @@ -0,0 +1,166 @@ +import requests +import json +import csv +import os + +def get_all_teams_and_users(api_token): + """ + Retrieves all teams and their users from OpsLevel using the GraphQL API. + + Args: + api_token (str): The OpsLevel API token. + + Returns: + list: A list of team data dictionaries. + """ + + url = "https://api.opslevel.com/graphql" + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + query = """ + query get_all_teams($endCursor: String) { + account { + teams(after: $endCursor) { + nodes { + id + alias + aliases + contacts { + type + displayName + address + } + memberships { + nodes { + user { + id + name + email + } + role + } + } + responsibilities + } + pageInfo{ + endCursor + hasNextPage + } + } + } + } + """ + + end_cursor = None + all_teams_data = [] + has_next_page = True + + while has_next_page: + variables = {"endCursor": end_cursor} + payload = {"query": query, "variables": variables} + + try: + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + data = response.json() + + if data and data.get("data") and data["data"].get("account") and data["data"]["account"].get("teams"): + teams = data["data"]["account"]["teams"]["nodes"] + all_teams_data.extend(teams) + + page_info = data["data"]["account"]["teams"]["pageInfo"] + end_cursor = page_info["endCursor"] + has_next_page = page_info["hasNextPage"] + else: + print("Error: Invalid response from OpsLevel API.") + return None + + except requests.exceptions.RequestException as e: + print(f"Error fetching data from OpsLevel API: {e}") + return None + + return all_teams_data + +def export_teams_and_users_to_csv(teams_data, output_csv_path): + """ + Exports team and user data to a CSV file. + + Args: + teams_data (list): A list of team data dictionaries. + output_csv_path (str, optional): The path to the CSV file to create. Defaults to "teams_and_users.csv". + """ + + try: + with open(output_csv_path, "w", newline="", encoding="utf-8") as csvfile: + writer = csv.writer(csvfile) + + writer.writerow([ + "Team ID", "Team Alias", "Team Aliases", "Contact Type", "Contact Display Name", + "Contact Address", "User ID", "User Name", "User Email", "Membership Role", "Responsibilities" + ]) + + for team in teams_data: + team_name = team.get("name", "") + team_alias = team.get("alias", "") + #team_aliases = ", ".join(team.get("aliases", [])) + + contacts = team.get("contacts", []) + memberships = team.get("memberships", {}).get("nodes", []) + + if contacts: + for contact in contacts: + contact_type = contact.get("type", "") + display_name = contact.get("displayName", "") + address = contact.get("address", "") + + if memberships: + for membership in memberships: + user = membership.get("user", {}) + user_id = user.get("id", "") + user_name = user.get("name", "") + user_email = user.get("email", "") + role = membership.get("role", "") + writer.writerow([ + team_name, team_alias, contact_type, display_name, + address, user_id, user_name, user_email, role + ]) + else: + writer.writerow([ + team_name, team_alias, contact_type, display_name, + address, "", "", "", "" + ]) + + elif memberships: + for membership in memberships: + user = membership.get("user", {}) + user_id = user.get("id", "") + user_name = user.get("name", "") + user_email = user.get("email", "") + role = membership.get("role", "") + writer.writerow([ + team_name, team_alias, "", "", "", user_id, user_name, user_email, role + ]) + else: + writer.writerow([team_name, team_alias, "", "", "", "", "", "", ""]) + + print(f"Data exported to {output_csv_path}") + + except IOError as e: + print(f"Error writing to CSV file: {e}") + +if __name__ == "__main__": + api_token = os.environ.get("OPSLEVEL_API_TOKEN") + output_csv_path = os.environ.get("OUTPUT_CSV_PATH") + + if not api_token or not output_csv_path: + print("Error: OPSLEVEL_API_TOKEN and OUTPUT_CSV_PATH environment variables must be set.") + exit(1) + + teams_data = get_all_teams_and_users(api_token) + if teams_data: + export_teams_and_users_to_csv(teams_data, output_csv_path) + diff --git a/scripts/wiz_integration/README.md b/scripts/wiz_integration/README.md new file mode 100644 index 0000000..2cb265d --- /dev/null +++ b/scripts/wiz_integration/README.md @@ -0,0 +1,99 @@ +# Wiz and OpsLevel Integration Script + +## Description + +This Python script (`sync_vulnerabilities.py`) integrates vulnerability data from the Wiz API with OpsLevel. It fetches vulnerability findings from Wiz, enriches them with service information from OpsLevel, and then creates or updates Code Issues and Code Issue Projects in OpsLevel. + +## Features + +* **Data Fetching from Wiz:** + * Retrieves vulnerability data from the Wiz API using GraphQL queries. + * Handles pagination to fetch all vulnerabilities. + * Extracts relevant vulnerability information, including name, ID, CVE description, severity, and affected asset details. +* **OpsLevel Integration:** + * Fetches service information from OpsLevel using GraphQL queries. + * Creates or reuses Code Issue Projects in OpsLevel to organize Wiz vulnerabilities. + * Connects Code Issue Projects to corresponding services in OpsLevel. + * Creates Code Issues in OpsLevel for each vulnerability, linking them to the appropriate Code Issue Project. +* **Error Handling:** + * Robust error handling for API requests, JSON parsing, and authentication. + * Prints detailed error messages to the console for debugging. +* **Configuration:** + * Uses environment variables for sensitive information like API tokens. + +## Requirements + +* Python 3.6 or later +* `requests` library (`pip install requests`) +* Environment variables: + * `OPSLEVEL_API_TOKEN`: Your OpsLevel API token. + * `WIZ_CLIENT_ID`: Your Wiz API client ID. + * `WIZ_CLIENT_SECRET`: Your Wiz API client secret. + +## Setup + +1. **Install Python:** Ensure you have Python 3.6 or later installed. +2. **Install `requests`:** + ```bash + pip install requests + ``` +3. **Set Environment Variables:** + * Set the `OPSLEVEL_API_TOKEN`, `WIZ_CLIENT_ID`, and `WIZ_CLIENT_SECRET` environment variables. You can do this in your shell's configuration file (e.g., `.bashrc`, `.zshrc`) or before running the script: + ```bash + export OPSLEVEL_API_TOKEN="your_opslevel_api_token" + export WIZ_CLIENT_ID="your_wiz_client_id" + export WIZ_CLIENT_SECRET="your_wiz_client_secret" + ``` + For Windows: + ```batch + set OPSLEVEL_API_TOKEN=your_opslevel_api_token + set WIZ_CLIENT_ID=your_wiz_client_id + set WIZ_CLIENT_SECRET=your_wiz_client_secret + ``` + +## Usage + +1. **Run the script:** + ```bash + python sync_vulnerabilities.py + ``` +2. **Output:** The script will print the processed vulnerability data as JSON to the console. It will also print the number of vulnerabilities found. Any errors during the process will also be printed to the console. + +## Script Details + +### Functions + +* `request_wiz_api_token(client_id, client_secret, token_url)`: Retrieves an OAuth access token from the Wiz API. +* `wiz_vulnerabilities_query()`: Returns the GraphQL query for fetching vulnerability data from Wiz. +* `call_wiz_api(api_url, headers, query, variables)`: Executes a GraphQL query against the Wiz API. +* `ol_service_info_query()`: Returns the GraphQL query for fetching service information from OpsLevel. +* `ol_codeissue_mutation()`: Returns the GraphQL mutation for creating/updating a code issue in OpsLevel. +* `ol_codeissueproject_mutation()`: Returns the GraphQL mutation for creating/updating a code issue *project* in OpsLevel. +* `ol_x_codeissue_svc_mutation()`: Returns the GraphQL mutation for connecting a code issue project to a service. +* `call_opslevel_api(query, variables, api_token)`: Executes a GraphQL query against the OpsLevel API. +* `check_for_cip(data, name)`: Checks if a code issue project with a given name exists in a list of projects. +* `sync_vulnerabilities()`: + * The main function that orchestrates the synchronization process. + * Fetches vulnerability data from Wiz. + * Fetches service data from OpsLevel. + * Creates or updates Code Issue Projects and Code Issues in OpsLevel. + * Handles errors and returns the processed data. + +### Workflow + +1. The `sync_vulnerabilities()` function is called. +2. It retrieves API tokens from environment variables. +3. It fetches vulnerability data from the Wiz API. +4. For each vulnerability, it fetches the corresponding service from OpsLevel. +5. It checks if a Code Issue Project for Wiz vulnerabilities exists for the service. If not, it creates one. +6. It connects the Code Issue Project to the service in OpsLevel. +7. It creates a Code Issue in OpsLevel for the vulnerability. +8. The processed data is returned and printed to the console. + +## Notes + +* Ensure that your API tokens are correctly set in the environment variables. +* The script uses a hardcoded OpsLevel service alias (`jtoebes-echo`). You may need to adjust this to match your OpsLevel setup. +* The script filters Wiz vulnerabilities to only process CRITICAL vulnerabilities. This can be changed in the `variables` definition in the `sync_vulnerabilities` function. +* The script assumes that the Wiz vulnerability data includes a tag with the key "Name" that corresponds to an OpsLevel service alias. +* The script includes error handling and will print error messages to the console. Check the console output for details if the script does not run successfully. diff --git a/scripts/wiz_integration/sync_wiz_vulnerabilities.py b/scripts/wiz_integration/sync_wiz_vulnerabilities.py new file mode 100644 index 0000000..ff929fc --- /dev/null +++ b/scripts/wiz_integration/sync_wiz_vulnerabilities.py @@ -0,0 +1,645 @@ +import requests +import json +import os +from typing import Dict, List, Optional, Any + +# Global variables +AUTH0_URLS = ['https://auth.wiz.io/oauth/token', 'https://auth0.gov.wiz.io/oauth/token', 'https://auth0.test.wiz.io/oauth/token', 'https://auth0.demo.wiz.io/oauth/token'] +COGNITO_URLS = ['https://auth.app.wiz.io/oauth/token', 'https://auth.gov.wiz.io/oauth/token', 'https://auth.test.wiz.io/oauth/token', 'https://auth.demo.wiz.io/oauth/token'] + + +# Standard headers +HEADERS_AUTH = {"Content-Type": "application/x-www-form-urlencoded"} +HEADERS = {"Content-Type": "application/json"} + +endpoint_url = "https://api.us17.app.wiz.io/graphql" +token_url = "https://auth.app.wiz.io/oauth/token" +integration_id = "Z2lkOi8vb3BzbGV2ZWwvSW50ZWdyYXRpb25zOjpDdXN0b21JbnRlZ3JhdGlvbi84OTcz" + +def request_wiz_api_token(client_id, client_secret, token_url): + """Retrieve an OAuth access token to be used against Wiz API""" + if token_url in AUTH0_URLS: + auth_payload = { + 'grant_type': 'client_credentials', + 'audience': 'beyond-api', + 'client_id': client_id, + 'client_secret': client_secret + } + elif token_url in COGNITO_URLS: + auth_payload = { + 'grant_type': 'client_credentials', + 'audience': 'wiz-api', + 'client_id': client_id, + 'client_secret': client_secret + } + else: + raise Exception('Invalid Token URL') + + # Uncomment the next first line and comment the line after that + # to run behind proxies + # response = requests.post(url=token_url, + # headers=HEADERS_AUTH, data=auth_payload, + # proxies=proxyDict) + response = requests.post(url=token_url, + headers=HEADERS_AUTH, data=auth_payload) + + if response.status_code != requests.codes.ok: + raise Exception('Error authenticating to Wiz [%d] - %s' % + (response.status_code, response.text)) + + try: + response_json = response.json() + TOKEN = response_json.get('access_token') + if not TOKEN: + message = 'Could not retrieve token from Wiz: {}'.format( + response_json.get("message")) + raise Exception(message) + except ValueError as exception: + print(exception) + raise Exception('Could not parse API response') + HEADERS["Authorization"] = "Bearer " + TOKEN + + return TOKEN + +def wiz_vulnerabilities_query(): + """ + Returns the GraphQL query and variables for fetching vulnerability data from the Wiz API. + + Returns: + tuple: A tuple containing the GraphQL query string and the variables dictionary. + """ + query = """ + query VulnerabilityFindingsPage($filterBy: VulnerabilityFindingFilters, $first: Int, $after: String, $orderBy: VulnerabilityFindingOrder) { + vulnerabilityFindings( + filterBy: $filterBy + first: $first + after: $after + orderBy: $orderBy + ) { + nodes { + id + portalUrl + name + CVEDescription + CVSSSeverity + score + exploitabilityScore + severity + nvdSeverity + weightedSeverity + impactScore + dataSourceName + hasExploit + hasCisaKevExploit + status + isHighProfileThreat + vendorSeverity + firstDetectedAt + lastDetectedAt + resolvedAt + description + remediation + detailedName + version + fixedVersion + detectionMethod + link + locationPath + artifactType { + ...SBOMArtifactTypeFragment + } + resolutionReason + epssSeverity + epssPercentile + epssProbability + validatedInRuntime + layerMetadata { + id + details + isBaseLayer + } + projects { + id + name + slug + businessUnit + riskProfile { + businessImpact + } + } + ignoreRules { + id + name + enabled + expiredAt + } + cvssv2 { + attackVector + attackComplexity + confidentialityImpact + integrityImpact + privilegesRequired + userInteractionRequired + } + cvssv3 { + attackVector + attackComplexity + confidentialityImpact + integrityImpact + privilegesRequired + userInteractionRequired + } + relatedIssueAnalytics { + issueCount + criticalSeverityCount + highSeverityCount + mediumSeverityCount + lowSeverityCount + informationalSeverityCount + } + cnaScore + vulnerableAsset { + ... on VulnerableAssetBase { + id + type + name + region + providerUniqueId + cloudProviderURL + cloudPlatform + nativeType + status + subscriptionName + subscriptionExternalId + subscriptionId + tags + hasLimitedInternetExposure + hasWideInternetExposure + isAccessibleFromVPN + isAccessibleFromOtherVnets + isAccessibleFromOtherSubscriptions + } + ... on VulnerableAssetVirtualMachine { + operatingSystem + ipAddresses + imageName + computeInstanceGroup { + id + externalId + name + replicaCount + tags + } + } + ... on VulnerableAssetServerless { + runtime + } + ... on VulnerableAssetContainerImage { + imageId + scanSource + registry { + name + externalId + } + repository { + name + externalId + } + executionControllers { + id + name + entityType + externalId + providerUniqueId + name + subscriptionExternalId + subscriptionId + subscriptionName + ancestors { + id + name + entityType + externalId + providerUniqueId + } + } + } + ... on VulnerableAssetContainer { + ImageExternalId + VmExternalId + ServerlessContainer + PodNamespace + PodName + NodeName + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + fragment SBOMArtifactTypeFragment on SBOMArtifactType { + group + codeLibraryLanguage + osPackageManager + hostedTechnology { + name + } + plugin + custom + } + """ + return query + +def call_wiz_api(api_url, headers, query, variables): + #print("Calling Wiz API") + """ + Executes the GraphQL query to fetch data from the Wiz API. + + Args: + api_url (str): The URL of the Wiz API. + headers (dict): The headers for the HTTP request. + query (str): The GraphQL query string. + variables (dict): The variables for the GraphQL query. + + Returns: + dict: The JSON response from the Wiz API, or None on error. + """ + payload = { + "query": query, + "variables": variables + } + try: + #print("Trying WIz API") + response = requests.post(api_url, headers=headers, json=payload) + #print(response.json) + response.raise_for_status() # Raise HTTPError for bad responses + return response.json() + except requests.exceptions.RequestException as e: + print(f"Error fetching data from Wiz API: {e}") + return None + except json.JSONDecodeError as e: + print(f"Error decoding JSON response: {e}, Response text: {response.text}") + return None + except Exception as e: + print(f"An unexpected error occurred: {e}") + return None + +def ol_service_info_query(): + """ + Returns the GraphQL query to retrieve service information from OpsLevel. + + Returns: + str: The GraphQL query string. + """ + return """ + query get_service_info_long($alias: String){ + account{ + service(alias: $alias){ + id + name + aliases + codeIssues { + totalCount + } + codeIssueProjects { + nodes { + name + externalId + id + } + } + } + } + } + """ + +def ol_codeissue_mutation(): + """ + Returns the GraphQL query to retrieve service information from OpsLevel. + """ + return """ + mutation codeIssueUpsert($input: CodeIssueInput!) { + codeIssueUpsert(input: $input) { + codeIssue { + id + name + externalUrl + issueType + severity + cves { + identifier + url + } + cwes{ + identifier + url + } + } + errors { + message + path + } + } + } + """ + +def ol_codeissueproject_mutation(): + return """ + mutation createCodeIssuProject($identifier: CodeIssueProjectIdentifierInput!, $name: String!, $url: String) { + codeIssueProjectUpsert(input: {identifier: $identifier, name: $name, url: $url}) { + codeIssueProject { + id + name + externalUrl + integration { + id + name + } + } + errors { + message + path + } + } + } + """ + +def ol_x_codeissue_svc_mutation(): + """ + Returns the GraphQL mutation to connect a code issue project to a service in OpsLevel. + """ + return """ + mutation codeIssueProjectResourceConnect($codeIssueProjectIds: [ID!]!, $resourceId: ID!) { + codeIssueProjectResourceConnect(input: { + codeIssueProjectIds: $codeIssueProjectIds, + resourceId: $resourceId + }) { + resource { + ... on Service { + id + name + } + ... on Repository { + id + name + } + } + errors { + message + path + } + } + } + """ + +def call_opslevel_api(query: str, variables: str, api_token: str) -> Optional[Dict]: + """ + Executes a GraphQL query against the OpsLevel API. + + Args: + query (str): The GraphQL query string. + variables (dict): A dictionary of variables for the query. + api_token (str): The OpsLevel API token. + + Returns: + Optional[Dict]: The JSON response from the API, or None on error. + """ + endpoint = "https://api.opslevel.com/graphql" + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json", + } + + opslevel_variables = variables + + payload = {"query": query, "variables": opslevel_variables} + #print((json.dumps(payload, indent=2))) + + try: + response = requests.post(endpoint, headers=headers, json=payload) + response.raise_for_status() + json_response = response.json() # Store the json + #print(f"OpsLevel API Response: {json_response}") # Print the response JSON + return response.json() + except requests.exceptions.RequestException as e: + print(f"Error: {e}") + print(f"Failed to connect to OpsLevel API. Please check your network connection and try again. Error Details: {e}") + return None + except json.JSONDecodeError as e: + print(f"Error decoding JSON: {e}. Response text: {response.text}") + print("The response from the OpsLevel API was not valid JSON. Please check the API response.") + return None + except Exception as e: + print(f"An unexpected error occurred: {e}") + print("Please check the script and your environment configuration.") + return None + +def check_for_cip(data, name): + """ + Checks if a dictionary with the name 'Wiz Issues' exists in the given list. + + Args: + data: A list of dictionaries, where each dictionary is expected to + have a 'name' key. + + Returns: + True if a dictionary with 'name' equal to 'Wiz Issues' is found, + False otherwise. + """ + #print(data) + #print(name) + for item in data: + #print(name) + print(item) + if isinstance(item, dict) and item.get('name') == name: + status = True + id = item.get('id') + return status, id + return False, None + +def sync_vulnerabilities(): + """ + Fetches vulnerability data from the Wiz API and extracts specific fields. + + Returns: + list: A list of dictionaries, where each dictionary represents a vulnerability + and contains the extracted fields. Returns an empty list on error. + """ + opslevel_token = os.environ.get("OPSLEVEL_API_TOKEN") + client_id = os.environ.get("WIZ_CLIENT_ID") + client_secret = os.environ.get("WIZ_CLIENT_SECRET") + api_url = "https://api.us17.app.wiz.io/graphql" # Or your specific Wiz API URL + + api_token = request_wiz_api_token(client_id, client_secret, token_url) # Get API token from environment variable + + if not api_token: + print("Error: WIZ_API_TOKEN environment variable not set.") + return [] # Return an empty list + if not opslevel_token: + print("Error: OPSLEVEL_API_TOKEN environment variable not set.") + return [] # Return an empty list + + headers = { + "accept": "application/json", + "authorization": f"Bearer {api_token}", + "content-type": "application/json", + } + + query = wiz_vulnerabilities_query() # Get query + opslevel_query = ol_service_info_query() + ol_cip_mutation = ol_codeissueproject_mutation() + opslevel_mutation = ol_x_codeissue_svc_mutation() # Get the mutation query + code_issue_mutation = ol_codeissue_mutation() + + #moved here + variables = { + "first": 100, # You can adjust the number of results per page + "filterBy": { + "updatedAt": { + "after": "2025-05-13T13:22:54.500Z" + }, + "relatedIssueSeverity": "CRITICAL" + }, + "orderBy": { + "field": "RELATED_ISSUE_SEVERITY", + "direction": "DESC" + } + } + + all_vulnerabilities = [] + has_next_page = True + after_cursor = None + + while has_next_page: + variables["after"] = after_cursor + try: # added try catch + response_data = call_wiz_api(api_url, headers, query, variables) # Call API + except Exception as e: + print(f"Error calling call_wiz_api: {e}") + return [] + + if response_data is None: + return [] # Error occurred in the call_wiz_api function + + if "data" in response_data and "vulnerabilityFindings" in response_data["data"]: + nodes = response_data["data"]["vulnerabilityFindings"]["nodes"] + page_info = response_data["data"]["vulnerabilityFindings"]["pageInfo"] + + #print(page_info["endCursor"]) + + for node in nodes: + # only include nodes with tags + #print(node["vulnerableAsset"]) + if "Name" in node["vulnerableAsset"]["tags"]: + opslevel_dependency_of = node["vulnerableAsset"]["tags"]["Name"] + extracted_data = { + "name": node["name"], + "external_id": node["id"], + "cve_description": node["CVEDescription"], + "first_detected_at": node["firstDetectedAt"], + "severity": node["severity"], + "portal_url": node["portalUrl"], + "status": node["status"], + "vulnerable_asset_type": node["vulnerableAsset"]["type"], + "vulnerable_asset_tags": node["vulnerableAsset"]["tags"], + "opslevel_dependency_of": opslevel_dependency_of + } + # Make OpsLevel API call if opslevel_dependency_of is found + opslevel_variables = { + "alias": "jtoebes-echo" + } + if opslevel_dependency_of: + opslevel_data = call_opslevel_api(opslevel_query, opslevel_variables, opslevel_token) + #print(opslevel_data) + if opslevel_data and "data" in opslevel_data and "account" in opslevel_data["data"] and "service" in opslevel_data["data"]["account"]: + service_data = opslevel_data["data"]["account"]["service"] + extracted_data["opslevel_service_id"] = service_data["id"] + extracted_data["opslevel_service_name"] = service_data["name"] + extracted_data["opslevel_service_aliases"] = service_data["aliases"] + extracted_data["opslevel_code_issues_total_count"] = service_data["codeIssues"]["totalCount"] + + # Check for Wiz code issues project + # print(service_data["codeIssueProjects"]["nodes"]) + extracted_data["wiz_code_issues_project_exists"] = any(project["name"] == service_data["codeIssueProjects"]["nodes"] for project in service_data["codeIssueProjects"]["nodes"]) + #print(extracted_data["wiz_code_issues_project_exists"]) + #print("Wiz Vulnerabilities: "+ service_data["name"]) + # connect the code issue project to the service if wiz_code_issues_project_exists is false + print(service_data["codeIssueProjects"]["nodes"]) + status, cip_id = check_for_cip(service_data["codeIssueProjects"]["nodes"], "Wiz Vulnerabilities: "+ service_data["name"]) + if not status: + print("Project connection does not exist for service") + opslevel_cipmutation_variables = { + "identifier": { + "integration": { + "id": integration_id # Use the provided integration_id + }, + "externalId": "Wiz Vulnerabilities: "+ service_data["name"] + }, + "url": endpoint_url, + "name": "Wiz Vulnerabilities: "+ service_data["name"] + } + ol_cipmutation_response = call_opslevel_api(ol_cip_mutation, opslevel_cipmutation_variables, opslevel_token) + #print(ol_cipmutation_response) + print("Code Issue project id: "+ ol_cipmutation_response["data"]["codeIssueProjectUpsert"]["codeIssueProject"]["id"]) + opslevel_mutation_variables = { + "resourceId": extracted_data["opslevel_service_id"], # service ID + "codeIssueProjectIds": ol_cipmutation_response["data"]["codeIssueProjectUpsert"]["codeIssueProject"]["id"] # Use the code issue project ID + } + opslevel_mutation_response = call_opslevel_api(opslevel_mutation, opslevel_mutation_variables, opslevel_token) + #print(opslevel_mutation_response) + if opslevel_mutation_response: + extracted_data["opslevel_code_issue_project_connection"] = opslevel_mutation_response["data"]["codeIssueProjectResourceConnect"]["resource"]["id"] + else: + extracted_data["opslevel_code_issue_project_connection_error"] = "Failed to connect code issue project" + else: + #extracted_data["opslevel_code_issue_project_connection"] = "Exists" + extracted_data["opslevel_code_issue_project_connection"] = cip_id + # print("Project connection exists") + else: + extracted_data["opslevel_error"] = "Service not found or error fetching data" + code_issue_variable = { + "input": { + "identifier": { + "codeIssueProject": { + "integration": { + "id": integration_id # This ID is a placeholder + }, + "externalId": "Wiz Vulnerabilities: "+ service_data["name"] + }, + "externalId": extracted_data["external_id"] + }, + "name": extracted_data["name"], + "issueCategory": "Infrastructure", # Hardcoded + "severity": extracted_data["severity"], + "cves": { + "identifier": ((extracted_data["cve_description"][:58] + '..') if len(extracted_data["cve_description"]) > 75 else extracted_data["cve_description"]), + "url": extracted_data["portal_url"], + } + } + } + print(code_issue_mutation) + print(code_issue_variable) + code_issue_response = call_opslevel_api(code_issue_mutation, code_issue_variable, opslevel_token) + print(extracted_data["opslevel_code_issue_project_connection"]) + print(code_issue_response) + all_vulnerabilities.append(extracted_data) + + has_next_page = page_info["hasNextPage"] + after_cursor = page_info["endCursor"] + else: + print("Error: \'data\' or \'vulnerabilityFindings\' not found in response.") + return [] + #print(all_vulnerabilities) + #print(len(all_vulnerabilities)) + return all_vulnerabilities + +if __name__ == "__main__": + vulnerabilities = sync_vulnerabilities() + if vulnerabilities: + print(json.dumps(vulnerabilities, indent=2)) # Pretty print the output + print(len(vulnerabilities)) + else: + print("No vulnerabilities found or error occurred.") \ No newline at end of file