From f7b24905e29727628ee892401c90da10b33674ad Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Sun, 22 Sep 2024 00:25:07 +0530 Subject: [PATCH 001/106] feat(python pkg)!:Initial python package files --- .gitignore | 3 + src/python/MANIFEST.in | 24 + src/python/README.md | 14 + src/python/config.ini | 7 + .../how2validate/{.gitkeep => __init__.py} | 0 .../how2validate/handler/validator_handler.py | 22 + .../how2validate/utility/config_utility.py | 52 + .../how2validate/utility/log_utility.py | 32 + .../how2validate/utility/tool_utility.py | 141 ++ src/python/how2validate/validator.py | 81 + .../validators/npm/npm_access_token.py | 121 ++ .../validators/snyk/snyk_auth_key.py | 119 ++ .../validators/sonarcloud/sonarcloud_token.py | 119 ++ src/python/main.py | 4 + src/python/requirements.in | 14 + src/python/requirements.txt | 80 + src/python/setup.py | 41 + src/python/tests/requirements.in | 2 + src/python/tests/requirements.txt | 20 + .../python/tests}/test_validator.py | 0 .../npm/validate_npm_access_token.py | 111 + src/python/tokenManager.json | 1799 +++++++++++++++++ 22 files changed, 2806 insertions(+) create mode 100644 src/python/MANIFEST.in create mode 100644 src/python/README.md create mode 100644 src/python/config.ini rename src/python/how2validate/{.gitkeep => __init__.py} (100%) create mode 100644 src/python/how2validate/handler/validator_handler.py create mode 100644 src/python/how2validate/utility/config_utility.py create mode 100644 src/python/how2validate/utility/log_utility.py create mode 100644 src/python/how2validate/utility/tool_utility.py create mode 100644 src/python/how2validate/validator.py create mode 100644 src/python/how2validate/validators/npm/npm_access_token.py create mode 100644 src/python/how2validate/validators/snyk/snyk_auth_key.py create mode 100644 src/python/how2validate/validators/sonarcloud/sonarcloud_token.py create mode 100644 src/python/main.py create mode 100644 src/python/requirements.in create mode 100644 src/python/requirements.txt create mode 100644 src/python/tests/requirements.in create mode 100644 src/python/tests/requirements.txt rename {tests/python => src/python/tests}/test_validator.py (100%) create mode 100644 src/python/tests/validator/npm/validate_npm_access_token.py create mode 100644 src/python/tokenManager.json diff --git a/.gitignore b/.gitignore index b62de68..5602238 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ npm-debug.log* # Production dist/ +# Build +build/ + # Docker docker-compose.yml .dockerignore diff --git a/src/python/MANIFEST.in b/src/python/MANIFEST.in new file mode 100644 index 0000000..bff3fa9 --- /dev/null +++ b/src/python/MANIFEST.in @@ -0,0 +1,24 @@ +# Include all Python files in the project +recursive-include how2validate *.py + +# Include non-Python files from 'how2validate' if necessary +recursive-include src/python/how2validate * + +# Exclude runtime-generated files and directories +prune build +prune dist + +# Exclude __pycache__ directories anywhere in the project +recursive-exclude * __pycache__ + +# Exclude compiled Python files and other binaries +global-exclude *.py[cod] +global-exclude *.so +global-exclude *.pyd +global-exclude *.dll +global-exclude *.exe +global-exclude *.log + +# Optionally, exclude other unwanted directories or files +prune tests/build +prune tests/dist diff --git a/src/python/README.md b/src/python/README.md new file mode 100644 index 0000000..9b45b53 --- /dev/null +++ b/src/python/README.md @@ -0,0 +1,14 @@ +# How2Validate + +How2Validate is a package designed to validate secrets and sensitive information across multiple platforms. + +## Features + +- Validate API keys, passwords, and other sensitive information. +- Cross-platform support (Windows, Linux, macOS). +- Easy integration with existing applications. + +## Installation + +```bash +pip install how2validate diff --git a/src/python/config.ini b/src/python/config.ini new file mode 100644 index 0000000..0980aba --- /dev/null +++ b/src/python/config.ini @@ -0,0 +1,7 @@ +[DEFAULT] +package_name = how2validate +version = 0.0.0a6 + +[SECRET] +secret_active = Active +secret_inactive = InActive \ No newline at end of file diff --git a/src/python/how2validate/.gitkeep b/src/python/how2validate/__init__.py similarity index 100% rename from src/python/how2validate/.gitkeep rename to src/python/how2validate/__init__.py diff --git a/src/python/how2validate/handler/validator_handler.py b/src/python/how2validate/handler/validator_handler.py new file mode 100644 index 0000000..751d89b --- /dev/null +++ b/src/python/how2validate/handler/validator_handler.py @@ -0,0 +1,22 @@ +# Create a mapping of service names to handler functions +from how2validate.validators.snyk.snyk_auth_key import validate_snyk_auth_key +from how2validate.validators.sonarcloud.sonarcloud_token import validate_sonarcloud_token +from how2validate.validators.npm.npm_access_token import validate_npm_access_token + + +service_handlers = { + "snyk_auth_key": validate_snyk_auth_key, + "sonarcloud_token": validate_sonarcloud_token, + "npm_access_token": validate_npm_access_token + # Add all your services here +} + +def validator_handle_service(service, secret, response, report): + # Get the handler function based on the service name + handler = service_handlers.get(service) + + if handler: + return handler(service, secret, response, report) + else: + return f"Error: No handler for service '{service}'" + diff --git a/src/python/how2validate/utility/config_utility.py b/src/python/how2validate/utility/config_utility.py new file mode 100644 index 0000000..26152e3 --- /dev/null +++ b/src/python/how2validate/utility/config_utility.py @@ -0,0 +1,52 @@ +import os +import configparser + +# Global variable to hold the configuration +config = None + +def init_config(): + global config + config = configparser.ConfigParser() + + # Get the directory of the current file + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # Path to the config.ini file + config_file_path = os.path.join(current_dir, '..', '..', 'config.ini') + + try: + config.read(config_file_path) + # Optionally handle any loading errors here + except FileNotFoundError: + print(f"Error: The file '{config_file_path}' was not found.") + +# Function to get the package name from the DEFAULT section +def get_package_name(): + if config: + return config.get('DEFAULT', 'package_name') + else: + raise ValueError("Configuration not initialized. Call init_config() first.") + +# Function to get the active secret status from the SECRET section +def get_active_secret_status(): + if config: + return config.get('SECRET', 'secret_active') + else: + raise ValueError("Configuration not initialized. Call init_config() first.") + +# Function to get the inactive secret status from the SECRET section +def get_inactive_secret_status(): + if config: + return config.get('SECRET', 'secret_inactive') + else: + raise ValueError("Configuration not initialized. Call init_config() first.") + +# Function to get the version from the DEFAULT section +def get_version(): + if config: + return config.get('DEFAULT', 'version') + else: + raise ValueError("Configuration not initialized. Call init_config() first.") + +# Initialization block to load the config when the module is imported or run +init_config() diff --git a/src/python/how2validate/utility/log_utility.py b/src/python/how2validate/utility/log_utility.py new file mode 100644 index 0000000..af7d494 --- /dev/null +++ b/src/python/how2validate/utility/log_utility.py @@ -0,0 +1,32 @@ +# src/python/how2validate/utility/logging_utility.py + +import logging +import sys + +from how2validate.utility.config_utility import get_active_secret_status, get_inactive_secret_status + +def setup_logging(): + logging.basicConfig( + level=logging.INFO, + format='%(message)s', + handlers=[logging.StreamHandler(sys.stdout)] + ) + + +def get_secret_status_message(service, is_active, response=None, response_data=None): + # Normalize is_active values to handle both 'Active' and 'InActive' + if is_active == get_active_secret_status(): + status = "active and operational" + elif is_active == get_inactive_secret_status(): + status = "inactive and not operational" + else: + raise ValueError(f"Unexpected is_active value: {is_active}. Expected 'Active' or 'InActive'.") + + # Base message about the secret's status + message = f"The provided secret '{service}' is currently {status}." + + # If a response exists, append it to the message + if response: + message += f" Here is the additional response data : \n{response_data}" + + return message diff --git a/src/python/how2validate/utility/tool_utility.py b/src/python/how2validate/utility/tool_utility.py new file mode 100644 index 0000000..cbecfb7 --- /dev/null +++ b/src/python/how2validate/utility/tool_utility.py @@ -0,0 +1,141 @@ +import argparse +import json +import logging +import os +import subprocess +import sys + +from how2validate.utility.config_utility import get_package_name +from how2validate.utility.log_utility import setup_logging + +# Call the logging setup function +setup_logging() + +# Get the directory of the current file +current_dir = os.path.dirname(__file__) + +# Path to the TokenManager JSON file +file_path = os.path.join(current_dir, '..', '..', 'tokenManager.json') + +def get_secretprovider(file_path = file_path): + with open(file_path, 'r') as f: + data = json.load(f) + + enabled_secrets_services = [] + + for provider, tokens in data.items(): + for token_info in tokens: + if token_info['is_enabled']: + enabled_secrets_services.append(f"{provider}") + + return enabled_secrets_services + +def get_secretservices(file_path = file_path): + with open(file_path, 'r') as f: + data = json.load(f) + + enabled_secrets_services = [] + + for provider, tokens in data.items(): + for token_info in tokens: + if token_info['is_enabled']: + enabled_secrets_services.append(f"{token_info['display_name']}") + + return enabled_secrets_services + +def format_serviceprovider(file_path = file_path): + """Format service choices as a bullet-point list.""" + with open(file_path, 'r') as f: + data = json.load(f) + + enabled_secrets_services = [] + + for provider, tokens in data.items(): + for token_info in tokens: + if token_info['is_enabled']: + enabled_secrets_services.append(f"{provider}") + + return "\n".join([f" - {service}" for service in enabled_secrets_services]) + +def format_services(file_path = file_path): + """Format service choices as a bullet-point list.""" + with open(file_path, 'r') as f: + data = json.load(f) + + enabled_secrets_services = [] + + for provider, tokens in data.items(): + for token_info in tokens: + if token_info['is_enabled']: + enabled_secrets_services.append(f"{provider} - {token_info['display_name']}") + + return "\n".join([f" - {service}" for service in enabled_secrets_services]) + +def format_string(input_string): + """ + Converts a string to lowercase and replaces spaces with underscores. + + Args: + input_string (str): The input string to format. + + Returns: + str: The formatted string with lowercase and underscores. + """ + if not isinstance(input_string, str): + raise ValueError("Input must be a string") + + return input_string.lower().replace(' ', '_') + +def validate_choice(value, valid_choices): + """ + Validates if the provided value is among the valid choices after formatting. + + Args: + value (str): The input value to validate. + valid_choices (list): The list of valid choices. + + Raises: + argparse.ArgumentTypeError: If the value is not in valid_choices. + + Returns: + str: The formatted and validated value. + """ + formatted_value = format_string(value) + formatted_choices = [format_string(choice) for choice in valid_choices] # Format valid choices + + if formatted_value not in formatted_choices: + raise argparse.ArgumentTypeError( + f"Invalid choice: '{value}'. Choose from {', '.join(valid_choices)}." + ) + + return formatted_value + +def redact_secret(secret): + """ + Redacts a secret by keeping the first 5 characters and replacing the rest with asterisks. + + Args: + secret (str): The secret string to redact. + + Returns: + str: The redacted secret. + """ + if not isinstance(secret, str): + raise ValueError("Input must be a string") + + if len(secret) <= 5: + return secret # Return the secret as is if it's 5 characters or less + + return secret[:5] + '*' * (len(secret) - 5) + +def update_tool(): + """Update the tool to the latest version.""" + logging.info("Updating the tool...") + # Use 'pip3' for Python 3.x and 'pip' for Python 2.x or if Python 3.x is the default interpreter + pip_command = "pip3" if sys.version_info.major == 3 else "pip" + try: + subprocess.run([pip_command, "install", "--upgrade", + f"{get_package_name()}"], check=True) + print("Tool updated to the latest version.") + except subprocess.CalledProcessError as e: + print(f"Failed to update the tool: {e}") \ No newline at end of file diff --git a/src/python/how2validate/validator.py b/src/python/how2validate/validator.py new file mode 100644 index 0000000..9aab639 --- /dev/null +++ b/src/python/how2validate/validator.py @@ -0,0 +1,81 @@ +import argparse +import logging + +from how2validate.utility.config_utility import get_active_secret_status, get_inactive_secret_status, get_package_name, get_version +from how2validate.utility.tool_utility import format_serviceprovider, format_services, get_secretprovider, get_secretservices, redact_secret, update_tool, validate_choice +from how2validate.utility.log_utility import setup_logging +from how2validate.handler.validator_handler import validator_handle_service + +# Call the logging setup function +setup_logging() + +# Custom formatter to remove choices display but keep custom help text +class CustomHelpFormatter(argparse.RawTextHelpFormatter): + def _get_help_string(self, action): + help_msg = action.help + if action.choices: # Remove choices from help string if present + help_msg = help_msg.split(" (choices:")[0] + return help_msg + +def parse_arguments(): + parser = argparse.ArgumentParser( + prog="How2Validate Tool", + description="Validate various types of secrets for different services.", + usage="%(prog)s", + epilog="Ensuring the authenticity of your secrets.", + formatter_class=CustomHelpFormatter) + + # Retrieve choices from environment variable + provider = get_secretprovider() + services = get_secretservices() + + # Define arguments + parser.add_argument('-provider', type=lambda s: validate_choice(s, provider), required=False, + help=f"Secret provider to validate secrets\nSupported providers:\n{format_serviceprovider()}") + parser.add_argument('-service', type=lambda s: validate_choice(s, services), required=False, + help=f"Service / SecretType to validate secrets\nSupported services:\n{format_services()}") + parser.add_argument('-secret', required=False, + help="Pass Secrets to be validated") + parser.add_argument('-r', '--response', action='store_true', + help=f"Prints {get_active_secret_status()}/ {get_inactive_secret_status()} upon validating secrets.") + parser.add_argument('-report', action='store_false', default=False, + help=f"Reports validated secrets over E-mail") + parser.add_argument('-v', '--version', action='version', version=f'How2Validate Tool version {get_version()}', + help='Expose the version') + parser.add_argument('--update', action='store_true', + help='Hack the tool to the latest version.') + + return parser.parse_args() + +def validate(provider,service, secret, response, report): + + logging.info(f"Started validating secret...") + result = validator_handle_service(service, secret, response, report) + logging.info(f"{result}") + return f"{result}" + +def main(args=None): + if args is None: + args = parse_arguments() + + if args.update: + try: + logging.info("Initiating tool update...") + update_tool() + logging.info("Tool updated successfully.") + except Exception as e: + logging.error(f"Error during tool update: {e}") + return + + if not args.provider or not args.service or not args.secret: + logging.error("Missing required arguments: -Provider, -Service, -Secret") + logging.error("Use '-h' or '--help' for usage information.") + return + + try: + logging.info(f"Initiating validation for service: {args.service} with secret: {redact_secret(args.secret)}") + result = validate(args.provider, args.service, args.secret, args.response, args.report) + logging.info("Validation completed successfully.") + except Exception as e: + logging.error(f"An error occurred during validation: {e}") + print(f"Error: {e}") \ No newline at end of file diff --git a/src/python/how2validate/validators/npm/npm_access_token.py b/src/python/how2validate/validators/npm/npm_access_token.py new file mode 100644 index 0000000..4038696 --- /dev/null +++ b/src/python/how2validate/validators/npm/npm_access_token.py @@ -0,0 +1,121 @@ +import json +import requests + +from how2validate.utility.config_utility import get_active_secret_status, get_inactive_secret_status +from how2validate.utility.log_utility import get_secret_status_message + +def validate_npm_access_token(service, secret, response, report): + """ + Validates the NPM access token by making a request to the NPM user API. + Raises an exception if the validation fails or returns an appropriate message if the token is inactive. + + Parameters: + - service (str): The name of the service. + - secret (str): The NPM access token (secret) to validate. + - response (bool): If True, the function will return the response data in addition to the status message. + - report (bool): Unused parameter, assumed for future extension. + + Returns: + - A status message indicating whether the secret is active or inactive. + - Optionally, the response data if `response` is True. + """ + + # NPM API endpoint for validating the token + url = "https://registry.npmjs.org/-/npm/v1/user" + + # Headers to ensure no caching and to authorize the request using the provided token + nocache_headers = {'Cache-Control': 'no-cache'} + headers_map = {'Authorization': f'Bearer {secret}'} + + try: + # Send a GET request to the NPM API with combined headers (nocache + authorization) + response_data = requests.get(url, headers={**nocache_headers, **headers_map}) + + # Raise an HTTPError if the response has an unsuccessful status code (4xx or 5xx) + response_data.raise_for_status() + + # Check if the request was successful (HTTP 200) + if response_data.status_code == 200: + # If `response` is False, return the active status message without response data + if not response: + return get_secret_status_message(service, get_active_secret_status(), response) + else: + # Attempt to parse the JSON response and return it + try: + json_response = response_data.json() + return get_secret_status_message(service, get_active_secret_status(), response, json.dumps(json_response, indent=4)) + except json.JSONDecodeError: + # Return an error message if the response isn't valid JSON + return get_secret_status_message(service, get_active_secret_status(), response, "Response is not a valid JSON.") + else: + # If the response status code is not 200, treat the secret as inactive + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + # Return the status message along with the raw text of the response + return get_secret_status_message(service, get_inactive_secret_status(), response, response_data.text) + + # Exception handling for specific HTTP errors + except requests.HTTPError as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text if e.response else "HTTPError response.") + + # General request exceptions (e.g., network issues) + except requests.RequestException as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle connection errors + except requests.ConnectionError as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle incorrect URLs + except requests.URLRequired as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle excessive redirects + except requests.TooManyRedirects as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle connection timeouts + except requests.ConnectTimeout as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle read timeouts + except requests.ReadTimeout as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle request timeouts in general + except requests.Timeout as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle JSON decoding errors + except json.JSONDecodeError: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + \ No newline at end of file diff --git a/src/python/how2validate/validators/snyk/snyk_auth_key.py b/src/python/how2validate/validators/snyk/snyk_auth_key.py new file mode 100644 index 0000000..dbe078d --- /dev/null +++ b/src/python/how2validate/validators/snyk/snyk_auth_key.py @@ -0,0 +1,119 @@ +import json +import requests + +from how2validate.utility.config_utility import get_active_secret_status, get_inactive_secret_status +from how2validate.utility.log_utility import get_secret_status_message + +def validate_snyk_auth_key(service, secret, response, report): + """ + Validates the Snyk API key by making a request to the Snyk user API. + Raises an exception if the validation fails or returns an appropriate message if the token is inactive. + + Parameters: + - service (str): The name of the service. + - secret (str): The Snyk API key (secret) to validate. + - response (bool): If True, the function will return the response data in addition to the status message. + - report (bool): Unused parameter, assumed for future extension. + + Returns: + - A status message indicating whether the secret is active or inactive. + - Optionally, the response data if `response` is True. + """ + + # Snyk API endpoint for getting the user information + url = "https://snyk.io/api/v1/user" + + # Headers to ensure no caching and to authorize the request using the provided API key (token) + nocache_headers = {'Cache-Control': 'no-cache'} + headers_map = {'Authorization': f'token {secret}'} + + try: + # Send a GET request to the Snyk API with combined headers (nocache + authorization) + response_data = requests.get(url, headers={**nocache_headers, **headers_map}) + + # Raise an HTTPError if the response has an unsuccessful status code (4xx or 5xx) + response_data.raise_for_status() + + # Check if the request was successful (HTTP 200) + if response_data.status_code == 200: + # If `response` is False, return the active status message without response data + if not response: + return get_secret_status_message(service, get_active_secret_status(), response) + else: + # Attempt to parse the JSON response and return it + try: + json_response = response_data.json() + return get_secret_status_message(service, get_active_secret_status(), response, json.dumps(json_response, indent=4)) + except json.JSONDecodeError: + # Return an error message if the response isn't valid JSON + return get_secret_status_message(service, get_active_secret_status(), response, "Response is not a valid JSON.") + else: + # If the response status code is not 200, treat the secret as inactive + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + # Return the status message along with the raw text of the response + return get_secret_status_message(service, get_inactive_secret_status(), response, response_data.text) + + # Exception handling for specific HTTP errors + except requests.HTTPError as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # General request exceptions (e.g., network issues) + except requests.RequestException as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle connection errors + except requests.ConnectionError as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle incorrect URLs + except requests.URLRequired as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle excessive redirects + except requests.TooManyRedirects as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle connection timeouts + except requests.ConnectTimeout as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle read timeouts + except requests.ReadTimeout as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle request timeouts in general + except requests.Timeout as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle JSON decoding errors + except json.JSONDecodeError as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) diff --git a/src/python/how2validate/validators/sonarcloud/sonarcloud_token.py b/src/python/how2validate/validators/sonarcloud/sonarcloud_token.py new file mode 100644 index 0000000..de7f463 --- /dev/null +++ b/src/python/how2validate/validators/sonarcloud/sonarcloud_token.py @@ -0,0 +1,119 @@ +import json +import requests + +from how2validate.utility.config_utility import get_active_secret_status, get_inactive_secret_status +from how2validate.utility.log_utility import get_secret_status_message + +def validate_sonarcloud_token(service, secret, response, report): + """ + Validates the Sonarcloud Token by making a request to the Sonarcloud user API. + Raises an exception if the validation fails or returns an appropriate message if the token is inactive. + + Parameters: + - service (str): The name of the service. + - secret (str): The Sonarcloud token (secret) to validate. + - response (bool): If True, the function will return the response data in addition to the status message. + - report (bool): Unused parameter, assumed for future extension. + + Returns: + - A status message indicating whether the secret is active or inactive. + - Optionally, the response data if `response` is True. + """ + + # Sonarcloud API endpoint for getting the current user's information + url = "https://sonarcloud.io/api/users/current" + + # Headers to ensure no caching and to authorize the request using the provided token + nocache_headers = {'Cache-Control': 'no-cache'} + headers_map = {'Authorization': f'Bearer {secret}'} + + try: + # Send a GET request to the Sonarcloud API with combined headers (nocache + authorization) + response_data = requests.get(url, headers={**nocache_headers, **headers_map}) + + # Raise an HTTPError if the response has an unsuccessful status code (4xx or 5xx) + response_data.raise_for_status() + + # Check if the request was successful (HTTP 200) + if response_data.status_code == 200: + # If `response` is False, return the active status message without response data + if not response: + return get_secret_status_message(service, get_active_secret_status(), response) + else: + # Attempt to parse the JSON response and return it + try: + json_response = response_data.json() + return get_secret_status_message(service, get_active_secret_status(), response, json.dumps(json_response, indent=4)) + except json.JSONDecodeError: + # Return an error message if the response isn't valid JSON + return get_secret_status_message(service, get_active_secret_status(), response, "Response is not a valid JSON.") + else: + # If the response status code is not 200, treat the secret as inactive + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + # Return the status message along with the raw text of the response + return get_secret_status_message(service, get_inactive_secret_status(), response, response_data.text) + + # Exception handling for specific HTTP errors + except requests.HTTPError as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # General request exceptions (e.g., network issues) + except requests.RequestException as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle connection errors + except requests.ConnectionError as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle incorrect URLs + except requests.URLRequired as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle excessive redirects + except requests.TooManyRedirects as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle connection timeouts + except requests.ConnectTimeout as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle read timeouts + except requests.ReadTimeout as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle request timeouts in general + except requests.Timeout as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) + + # Handle JSON decoding errors + except json.JSONDecodeError as e: + if not response: + return get_secret_status_message(service, get_inactive_secret_status(), response) + else: + return get_secret_status_message(service, get_inactive_secret_status(), response, e.response.text) \ No newline at end of file diff --git a/src/python/main.py b/src/python/main.py new file mode 100644 index 0000000..360cb4d --- /dev/null +++ b/src/python/main.py @@ -0,0 +1,4 @@ +from how2validate.validator import main + +if __name__ == "__main__": + main() diff --git a/src/python/requirements.in b/src/python/requirements.in new file mode 100644 index 0000000..b3e275d --- /dev/null +++ b/src/python/requirements.in @@ -0,0 +1,14 @@ +# This is the optimised requirements.in file using +# pip-tools to get all the dependent dependencies +# +# Following below command to get autogenerated requirements.txt +# pip install pip-tools +# pip-compile ./requirements.in +# +requests +setuptools +twine +pyOpenSSL +wheel +pymongo +python-dotenv \ No newline at end of file diff --git a/src/python/requirements.txt b/src/python/requirements.txt new file mode 100644 index 0000000..ac568bf --- /dev/null +++ b/src/python/requirements.txt @@ -0,0 +1,80 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile ./requirements.in +# +certifi==2024.8.30 + # via requests +cffi==1.17.1 + # via cryptography +charset-normalizer==3.3.2 + # via requests +cryptography==43.0.1 + # via pyopenssl +dnspython==2.6.1 + # via pymongo +docutils==0.21.2 + # via readme-renderer +idna==3.10 + # via requests +importlib-metadata==8.5.0 + # via twine +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.0.1 + # via keyring +jaraco-functools==4.0.2 + # via keyring +keyring==25.4.0 + # via twine +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +more-itertools==10.5.0 + # via + # jaraco-classes + # jaraco-functools +nh3==0.2.18 + # via readme-renderer +pkginfo==1.10.0 + # via twine +pycparser==2.22 + # via cffi +pygments==2.18.0 + # via + # readme-renderer + # rich +pymongo==4.9.1 + # via -r ./requirements.in +pyopenssl==24.2.1 + # via -r ./requirements.in +python-dotenv==1.0.1 + # via -r ./requirements.in +readme-renderer==44.0 + # via twine +requests==2.32.3 + # via + # -r ./requirements.in + # requests-toolbelt + # twine +requests-toolbelt==1.0.0 + # via twine +rfc3986==2.0.0 + # via twine +rich==13.8.1 + # via twine +twine==5.1.1 + # via -r ./requirements.in +urllib3==2.2.3 + # via + # requests + # twine +wheel==0.44.0 + # via -r ./requirements.in +zipp==3.20.2 + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/src/python/setup.py b/src/python/setup.py index e69de29..3250ffa 100644 --- a/src/python/setup.py +++ b/src/python/setup.py @@ -0,0 +1,41 @@ +from setuptools import setup, find_packages +import os + +from how2validate.utility.config_utility import get_version + +# Retrieve the current version from environment variable +version = get_version() + +# Get the path to requirements.txt which is one folder up +requirements_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '.', 'requirements.txt')) + +# Read requirements from requirements.txt +with open(requirements_path, 'r') as f: + requirements = f.read().splitlines() + +# Read the README.md file +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setup( + name='how2validate', + version=version, + description='A cli and package for validating secrets.', + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/Blackplums/how2validate", + author='Vigneshkna', + author_email='vigneshkna@gmail.com', + license='MIT', + packages=find_packages(), + include_package_data=True, + package_data={ + '': ['*.txt', '../requirements.txt', '../config.ini', '../tokenManager.json', '../README.md', '../main.py'], + }, + install_requires=requirements, + entry_points={ + 'console_scripts': [ + 'how2validate=how2validate.validator:main', + ], + }, +) diff --git a/src/python/tests/requirements.in b/src/python/tests/requirements.in new file mode 100644 index 0000000..1a1c032 --- /dev/null +++ b/src/python/tests/requirements.in @@ -0,0 +1,2 @@ +requests-mock +requests diff --git a/src/python/tests/requirements.txt b/src/python/tests/requirements.txt new file mode 100644 index 0000000..7749f16 --- /dev/null +++ b/src/python/tests/requirements.txt @@ -0,0 +1,20 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile ./requirements.in +# +certifi==2024.8.30 + # via requests +charset-normalizer==3.3.2 + # via requests +idna==3.10 + # via requests +requests==2.32.3 + # via + # -r ./requirements.in + # requests-mock +requests-mock==1.12.1 + # via -r ./requirements.in +urllib3==2.2.3 + # via requests diff --git a/tests/python/test_validator.py b/src/python/tests/test_validator.py similarity index 100% rename from tests/python/test_validator.py rename to src/python/tests/test_validator.py diff --git a/src/python/tests/validator/npm/validate_npm_access_token.py b/src/python/tests/validator/npm/validate_npm_access_token.py new file mode 100644 index 0000000..21c0e79 --- /dev/null +++ b/src/python/tests/validator/npm/validate_npm_access_token.py @@ -0,0 +1,111 @@ +import unittest +from unittest.mock import patch, Mock +import requests +import json + +from how2validate.utility.config_utility import get_active_secret_status, get_inactive_secret_status +from how2validate.validators.npm.npm_access_token import validate_npm_access_token + + + +# Import the function you want to test + +class TestValidateNpmAccessToken(unittest.TestCase): + + @patch('how2validate.validators.npm.npm_access_token.requests.get') + def test_valid_token_success(self, mock_get): + """ + Test the case where the NPM access token is valid and the API returns 200 with a valid JSON response. + """ + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"user": "valid_user"} + mock_get.return_value = mock_response + + result = validate_npm_access_token("npm", "valid_token", True, False) + + self.assertIn("active and operational", result.lower()) + self.assertIn('"user": "valid_user"', result) + + @patch('how2validate.validators.npm.npm_access_token.requests.get') + def test_inactive_token(self, mock_get): + """ + Test the case where the NPM access token is invalid, and the API returns a 401 status code. + """ + mock_response = Mock() + mock_response.status_code = 401 + mock_response.text = "Unauthorized" + mock_get.return_value = mock_response + + result = validate_npm_access_token("npm", "invalid_token", True, False) + + self.assertIn("inactive and not operational", result) + self.assertIn("Unauthorized", result) + + # @patch('how2validate.validators.npm.npm_access_token.requests.get') + # @patch('how2validate.utility.log_utility.get_active_secret_status') + # def test_json_decode_error(self, mock_get_status, mock_get): + # """ + # Test the case where the NPM API returns a 200 response but the body is not valid JSON. + # """ + # mock_response = Mock() + # mock_response.status_code = 200 + # mock_response.json.side_effect = ValueError("Invalid JSON") # Simulate JSON decode error + # mock_get.return_value = mock_response + + # # Mock the active secret status return value + # mock_get_status.return_value = "Active" + + # result = validate_npm_access_token("npm", "valid_token", True, False) + + # self.assertIn("Response is not a valid JSON.", result) + + + # @patch('how2validate.validators.npm.npm_access_token.requests.get') + # def test_connection_error(self, mock_get): + # """ + # Test the case where a connection error occurs during the request. + # """ + # mock_get.side_effect = requests.ConnectionError("Failed to establish a connection") + + # result = validate_npm_access_token("npm", "invalid_token", True, False) + + # self.assertIn("currently inactive and not operational", result) + # self.assertIn("No response.", result) + + # @patch('how2validate.validators.npm.npm_access_token.requests.get') + # def test_timeout_error(self, mock_get): + # """ + # Test the case where a timeout occurs during the request. + # """ + # mock_get.side_effect = requests.Timeout("Request timed out") + + # result = validate_npm_access_token("npm", "invalid_token", True, False) + + # self.assertIn("currently inactive and not operational", result) + # self.assertIn("Request timed out", result) + + # @patch('how2validate.validators.npm.npm_access_token.requests.get') + # def test_empty_response(self, mock_get): + # """ + # Test the case where the API returns a 200 response with an empty body. + # """ + # mock_response = MagicMock() + # mock_response.status_code = 200 + # mock_response.json.return_value = {} # Return an empty JSON object + # mock_get.return_value = mock_response + + # service = "npm" + # secret = "dummy_secret" + # response = True + # report = False + + # result = validate_npm_access_token(service, secret, response, report) + + # # Check for "active" status message and ensure it's structured correctly + # self.assertIn("currently active and operational", result) + # self.assertIn("Here is the additional response data :", result) + # self.assertIn("{}", result) + +if __name__ == '__main__': + unittest.main() diff --git a/src/python/tokenManager.json b/src/python/tokenManager.json new file mode 100644 index 0000000..1bcd033 --- /dev/null +++ b/src/python/tokenManager.json @@ -0,0 +1,1799 @@ +{ + "Adafruit": [ + { + "secret_type": "adafruit_io_key", + "display_name": "Adafruit Io Key", + "is_enabled": false + } + ], + "Adobe": [ + { + "secret_type": "adobe_client_secret", + "display_name": "Adobe Client Secret", + "is_enabled": false + }, + { + "secret_type": "adobe_device_token", + "display_name": "Adobe Device Token", + "is_enabled": false + }, + { + "secret_type": "adobe_pac_token", + "display_name": "Adobe Pac Token", + "is_enabled": false + }, + { + "secret_type": "adobe_refresh_token", + "display_name": "Adobe Refresh Token", + "is_enabled": false + }, + { + "secret_type": "adobe_service_token", + "display_name": "Adobe Service Token", + "is_enabled": false + }, + { + "secret_type": "adobe_short_lived_access_token", + "display_name": "Adobe Short Lived Access Token", + "is_enabled": false + } + ], + "Aiven": [ + { + "secret_type": "aiven_auth_token", + "display_name": "Aiven Auth Token", + "is_enabled": false + }, + { + "secret_type": "aiven_service_password", + "display_name": "Aiven Service Password", + "is_enabled": false + } + ], + "Alibaba": [ + { + "secret_type": "alibaba_cloud_access_key", + "display_name": "Alibaba Cloud Access Key", + "is_enabled": false + } + ], + "Amazon": [ + { + "secret_type": "amazon_oauth_client", + "display_name": "Amazon Oauth Client", + "is_enabled": false + } + ], + "Amazon AWS": [ + { + "secret_type": "aws_access_key_id", + "display_name": "Aws Access Key Id", + "is_enabled": false + }, + { + "secret_type": "aws_secret_access_key", + "display_name": "Aws Secret Access Key", + "is_enabled": false + }, + { + "secret_type": "aws_session_token", + "display_name": "Aws Session Token", + "is_enabled": false + }, + { + "secret_type": "aws_temporary_access_key_id", + "display_name": "Aws Temporary Access Key Id", + "is_enabled": false + } + ], + "Anthropic": [ + { + "secret_type": "anthropic_api_key", + "display_name": "Anthropic Api Key", + "is_enabled": false + }, + { + "secret_type": "anthropic_session_id", + "display_name": "Anthropic Session Id", + "is_enabled": false + } + ], + "Asana": [ + { + "secret_type": "asana_legacy_format_personal_access_token", + "display_name": "Asana Legacy Format Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "asana_personal_access_token", + "display_name": "Asana Personal Access Token", + "is_enabled": false + } + ], + "Atlassian": [ + { + "secret_type": "atlassian_api_token", + "display_name": "Atlassian Api Token", + "is_enabled": false + }, + { + "secret_type": "atlassian_jwt", + "display_name": "Atlassian Jwt", + "is_enabled": false + } + ], + "Authress": [ + { + "secret_type": "authress_service_client_access_key", + "display_name": "Authress Service Client Access Key", + "is_enabled": false + } + ], + "Azure": [ + { + "secret_type": "azure_active_directory_application_secret", + "display_name": "Azure Active Directory Application Secret", + "is_enabled": false + }, + { + "secret_type": "azure_active_directory_user_credential", + "display_name": "Azure Active Directory User Credential", + "is_enabled": false + }, + { + "secret_type": "azure_apim_direct_management_key", + "display_name": "Azure Apim Direct Management Key", + "is_enabled": false + }, + { + "secret_type": "azure_apim_gateway_key", + "display_name": "Azure Apim Gateway Key", + "is_enabled": false + }, + { + "secret_type": "azure_apim_repository_key", + "display_name": "Azure Apim Repository Key", + "is_enabled": false + }, + { + "secret_type": "azure_apim_subscription_key", + "display_name": "Azure Apim Subscription Key", + "is_enabled": false + }, + { + "secret_type": "azure_app_configuration_connection_string", + "display_name": "Azure App Configuration Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_batch_key_identifiable", + "display_name": "Azure Batch Key Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_cache_for_redis_access_key", + "display_name": "Azure Cache For Redis Access Key", + "is_enabled": false + }, + { + "secret_type": "azure_common_annotated_security_key", + "display_name": "Azure Common Annotated Security Key", + "is_enabled": false + }, + { + "secret_type": "azure_communication_services_connection_string", + "display_name": "Azure Communication Services Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_container_registry_key_identifiable", + "display_name": "Azure Container Registry Key Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_cosmosdb_key_identifiable", + "display_name": "Azure Cosmosdb Key Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_devops_personal_access_token", + "display_name": "Azure Devops Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "azure_event_hub_key_identifiable", + "display_name": "Azure Event Hub Key Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_function_key", + "display_name": "Azure Function Key", + "is_enabled": false + }, + { + "secret_type": "azure_iot_device_connection_string", + "display_name": "Azure Iot Device Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_iot_device_key", + "display_name": "Azure Iot Device Key", + "is_enabled": false + }, + { + "secret_type": "azure_iot_device_provisioning_key", + "display_name": "Azure Iot Device Provisioning Key", + "is_enabled": false + }, + { + "secret_type": "azure_iot_hub_connection_string", + "display_name": "Azure Iot Hub Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_iot_hub_key", + "display_name": "Azure Iot Hub Key", + "is_enabled": false + }, + { + "secret_type": "azure_iot_provisioning_connection_string", + "display_name": "Azure Iot Provisioning Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_management_certificate", + "display_name": "Azure Management Certificate", + "is_enabled": false + }, + { + "secret_type": "azure_ml_web_service_classic_identifiable_key", + "display_name": "Azure Ml Web Service Classic Identifiable Key", + "is_enabled": false + }, + { + "secret_type": "azure_relay_key_identifiable", + "display_name": "Azure Relay Key Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_sas_token", + "display_name": "Azure Sas Token", + "is_enabled": false + }, + { + "secret_type": "azure_search_admin_key", + "display_name": "Azure Search Admin Key", + "is_enabled": false + }, + { + "secret_type": "azure_search_query_key", + "display_name": "Azure Search Query Key", + "is_enabled": false + }, + { + "secret_type": "azure_service_bus_identifiable", + "display_name": "Azure Service Bus Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_signalr_connection_string", + "display_name": "Azure Signalr Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_sql_connection_string", + "display_name": "Azure Sql Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_sql_password", + "display_name": "Azure Sql Password", + "is_enabled": false + }, + { + "secret_type": "azure_storage_account_key", + "display_name": "Azure Storage Account Key", + "is_enabled": false + }, + { + "secret_type": "azure_web_pub_sub_connection_string", + "display_name": "Azure Web Pub Sub Connection String", + "is_enabled": false + }, + { + "secret_type": "microsoft_corporate_network_user_credential", + "display_name": "Microsoft Corporate Network User Credential", + "is_enabled": false + } + ], + "Baidu": [ + { + "secret_type": "baiducloud_api_accesskey", + "display_name": "Baiducloud Api Accesskey", + "is_enabled": false + } + ], + "Beamer": [ + { + "secret_type": "beamer_api_key", + "display_name": "Beamer Api Key", + "is_enabled": false + } + ], + "Bitbucket": [ + { + "secret_type": "bitbucket_server_personal_access_token", + "display_name": "Bitbucket Server Personal Access Token", + "is_enabled": false + } + ], + "Canadian Digital Service": [ + { + "secret_type": "cds_canada_notify_api_key", + "display_name": "Cds Canada Notify Api Key", + "is_enabled": false + } + ], + "Canva": [ + { + "secret_type": "canva_app_secret", + "display_name": "Canva App Secret", + "is_enabled": false + }, + { + "secret_type": "canva_connect_api_secret", + "display_name": "Canva Connect Api Secret", + "is_enabled": false + }, + { + "secret_type": "canva_secret", + "display_name": "Canva Secret", + "is_enabled": false + } + ], + "Cashfree": [ + { + "secret_type": "cashfree_api_key", + "display_name": "Cashfree Api Key", + "is_enabled": false + } + ], + "Checkout.com": [ + { + "secret_type": "checkout_production_secret_key", + "display_name": "Checkout Production Secret Key", + "is_enabled": false + }, + { + "secret_type": "checkout_test_secret_key", + "display_name": "Checkout Test Secret Key", + "is_enabled": false + } + ], + "Chief Tools": [ + { + "secret_type": "chief_tools_token", + "display_name": "Chief Tools Token", + "is_enabled": false + } + ], + "CircleCI": [ + { + "secret_type": "circleci_bot_access_token", + "display_name": "Circleci Bot Access Token", + "is_enabled": false + }, + { + "secret_type": "circleci_personal_access_token", + "display_name": "Circleci Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "circleci_project_access_token", + "display_name": "Circleci Project Access Token", + "is_enabled": false + }, + { + "secret_type": "circleci_release_integration_token", + "display_name": "Circleci Release Integration Token", + "is_enabled": false + } + ], + "Clojars": [ + { + "secret_type": "clojars_deploy_token", + "display_name": "Clojars Deploy Token", + "is_enabled": false + } + ], + "CloudBees": [ + { + "secret_type": "codeship_credential", + "display_name": "Codeship Credential", + "is_enabled": false + } + ], + "Contentful": [ + { + "secret_type": "contentful_personal_access_token", + "display_name": "Contentful Personal Access Token", + "is_enabled": false + } + ], + "Contributed Systems": [ + { + "secret_type": "contributed_systems_credentials", + "display_name": "Contributed Systems Credentials", + "is_enabled": false + } + ], + "Coveo": [ + { + "secret_type": "coveoaccesstoken", + "display_name": "Coveoaccesstoken", + "is_enabled": false + }, + { + "secret_type": "coveoapikey", + "display_name": "Coveoapikey", + "is_enabled": false + } + ], + "crates.io": [ + { + "secret_type": "cratesio_api_token", + "display_name": "Cratesio Api Token", + "is_enabled": false + } + ], + "Databricks": [ + { + "secret_type": "databricks_access_token", + "display_name": "Databricks Access Token", + "is_enabled": false + } + ], + "Datadog": [ + { + "secret_type": "datadog_api_key", + "display_name": "Datadog Api Key", + "is_enabled": false + }, + { + "secret_type": "datadog_app_key", + "display_name": "Datadog App Key", + "is_enabled": false + } + ], + "Defined Networking": [ + { + "secret_type": "defined_networking_nebula_api_key", + "display_name": "Defined Networking Nebula Api Key", + "is_enabled": false + } + ], + "DevCycle": [ + { + "secret_type": "devcycle_client_api_key", + "display_name": "Devcycle Client Api Key", + "is_enabled": false + }, + { + "secret_type": "devcycle_mobile_api_key", + "display_name": "Devcycle Mobile Api Key", + "is_enabled": false + }, + { + "secret_type": "devcycle_server_api_key", + "display_name": "Devcycle Server Api Key", + "is_enabled": false + } + ], + "DigitalOcean": [ + { + "secret_type": "digitalocean_oauth_token", + "display_name": "Digitalocean Oauth Token", + "is_enabled": false + }, + { + "secret_type": "digitalocean_personal_access_token", + "display_name": "Digitalocean Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "digitalocean_refresh_token", + "display_name": "Digitalocean Refresh Token", + "is_enabled": false + }, + { + "secret_type": "digitalocean_system_token", + "display_name": "Digitalocean System Token", + "is_enabled": false + } + ], + "Discord": [ + { + "secret_type": "discord_bot_token", + "display_name": "Discord Bot Token", + "is_enabled": false + } + ], + "Docker": [ + { + "secret_type": "docker_personal_access_token", + "display_name": "Docker Personal Access Token", + "is_enabled": false + } + ], + "Doppler": [ + { + "secret_type": "doppler_audit_token", + "display_name": "Doppler Audit Token", + "is_enabled": false + }, + { + "secret_type": "doppler_cli_token", + "display_name": "Doppler Cli Token", + "is_enabled": false + }, + { + "secret_type": "doppler_personal_token", + "display_name": "Doppler Personal Token", + "is_enabled": false + }, + { + "secret_type": "doppler_scim_token", + "display_name": "Doppler Scim Token", + "is_enabled": false + }, + { + "secret_type": "doppler_service_account_token", + "display_name": "Doppler Service Account Token", + "is_enabled": false + }, + { + "secret_type": "doppler_service_token", + "display_name": "Doppler Service Token", + "is_enabled": false + } + ], + "Dropbox": [ + { + "secret_type": "dropbox_access_token", + "display_name": "Dropbox Access Token", + "is_enabled": false + }, + { + "secret_type": "dropbox_short_lived_access_token", + "display_name": "Dropbox Short Lived Access Token", + "is_enabled": false + } + ], + "Duffel": [ + { + "secret_type": "duffel_live_access_token", + "display_name": "Duffel Live Access Token", + "is_enabled": false + }, + { + "secret_type": "duffel_test_access_token", + "display_name": "Duffel Test Access Token", + "is_enabled": false + } + ], + "Dynatrace": [ + { + "secret_type": "dynatrace_api_token", + "display_name": "Dynatrace Api Token", + "is_enabled": false + }, + { + "secret_type": "dynatrace_internal_token", + "display_name": "Dynatrace Internal Token", + "is_enabled": false + } + ], + "EasyPost": [ + { + "secret_type": "easypost_production_api_key", + "display_name": "Easypost Production Api Key", + "is_enabled": false + }, + { + "secret_type": "easypost_test_api_key", + "display_name": "Easypost Test Api Key", + "is_enabled": false + } + ], + "eBay": [ + { + "secret_type": "ebay_production_client", + "display_name": "Ebay Production Client", + "is_enabled": false + }, + { + "secret_type": "ebay_sandbox_client", + "display_name": "Ebay Sandbox Client", + "is_enabled": false + } + ], + "Facebook": [ + { + "secret_type": "facebook_access_token", + "display_name": "Facebook Access Token", + "is_enabled": false + } + ], + "Fastly": [ + { + "secret_type": "fastly_api_token", + "display_name": "Fastly Api Token", + "is_enabled": false + } + ], + "Figma": [ + { + "secret_type": "figma_pat", + "display_name": "Figma Pat", + "is_enabled": false + } + ], + "Finicity": [ + { + "secret_type": "finicity_app_key", + "display_name": "Finicity App Key", + "is_enabled": false + } + ], + "Firebase": [ + { + "secret_type": "firebase_cloud_messaging_server_key", + "display_name": "Firebase Cloud Messaging Server Key", + "is_enabled": false + } + ], + "Flickr": [ + { + "secret_type": "flickr_api_key", + "display_name": "Flickr Api Key", + "is_enabled": false + } + ], + "Flutterwave": [ + { + "secret_type": "flutterwave_live_api_secret_key", + "display_name": "Flutterwave Live Api Secret Key", + "is_enabled": false + }, + { + "secret_type": "flutterwave_test_api_secret_key", + "display_name": "Flutterwave Test Api Secret Key", + "is_enabled": false + } + ], + "Frame.io": [ + { + "secret_type": "frameio_developer_token", + "display_name": "Frameio Developer Token", + "is_enabled": false + }, + { + "secret_type": "frameio_jwt", + "display_name": "Frameio Jwt", + "is_enabled": false + } + ], + "FullStory": [ + { + "secret_type": "fullstory_api_key", + "display_name": "Fullstory Api Key", + "is_enabled": false + } + ], + "GitHub": [ + { + "secret_type": "github_app_installation_access_token", + "display_name": "Github App Installation Access Token", + "is_enabled": false + }, + { + "secret_type": "github_oauth_access_token", + "display_name": "Github Oauth Access Token", + "is_enabled": false + }, + { + "secret_type": "github_personal_access_token", + "display_name": "Github Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "github_refresh_token", + "display_name": "Github Refresh Token", + "is_enabled": false + }, + { + "secret_type": "github_ssh_private_key", + "display_name": "Github Ssh Private Key", + "is_enabled": false + }, + { + "secret_type": "github_test_token", + "display_name": "Github Test Token", + "is_enabled": false + } + ], + "GitHub Secret Scanning": [ + { + "secret_type": "secret_scanning_sample_token", + "display_name": "Secret Scanning Sample Token", + "is_enabled": false + } + ], + "GitLab": [ + { + "secret_type": "gitlab_access_token", + "display_name": "Gitlab Access Token", + "is_enabled": false + } + ], + "GoCardless": [ + { + "secret_type": "gocardless_live_access_token", + "display_name": "Gocardless Live Access Token", + "is_enabled": false + }, + { + "secret_type": "gocardless_sandbox_access_token", + "display_name": "Gocardless Sandbox Access Token", + "is_enabled": false + } + ], + "Google": [ + { + "secret_type": "google_api_key", + "display_name": "Google Api Key", + "is_enabled": false + }, + { + "secret_type": "google_cloud_private_key_id", + "display_name": "Google Cloud Private Key Id", + "is_enabled": false + }, + { + "secret_type": "google_cloud_service_account_credentials", + "display_name": "Google Cloud Service Account Credentials", + "is_enabled": false + }, + { + "secret_type": "google_cloud_storage_access_key_secret", + "display_name": "Google Cloud Storage Access Key Secret", + "is_enabled": false + }, + { + "secret_type": "google_cloud_storage_service_account_access_key_id", + "display_name": "Google Cloud Storage Service Account Access Key Id", + "is_enabled": false + }, + { + "secret_type": "google_cloud_storage_access_key_secret", + "display_name": "Google Cloud Storage Access Key Secret", + "is_enabled": false + }, + { + "secret_type": "google_cloud_storage_user_access_key_id", + "display_name": "Google Cloud Storage User Access Key Id", + "is_enabled": false + }, + { + "secret_type": "google_oauth_access_token", + "display_name": "Google Oauth Access Token", + "is_enabled": false + }, + { + "secret_type": "google_oauth_client_id", + "display_name": "Google Oauth Client Id", + "is_enabled": false + }, + { + "secret_type": "google_oauth_client_secret", + "display_name": "Google Oauth Client Secret", + "is_enabled": false + }, + { + "secret_type": "google_oauth_refresh_token", + "display_name": "Google Oauth Refresh Token", + "is_enabled": false + } + ], + "Grafana": [ + { + "secret_type": "grafana_cloud_api_key", + "display_name": "Grafana Cloud Api Key", + "is_enabled": false + }, + { + "secret_type": "grafana_cloud_api_token", + "display_name": "Grafana Cloud Api Token", + "is_enabled": false + }, + { + "secret_type": "grafana_project_api_key", + "display_name": "Grafana Project Api Key", + "is_enabled": false + }, + { + "secret_type": "grafana_project_service_account_token", + "display_name": "Grafana Project Service Account Token", + "is_enabled": false + } + ], + "HashiCorp": [ + { + "secret_type": "hashicorp_vault_batch_token", + "display_name": "Hashicorp Vault Batch Token", + "is_enabled": false + }, + { + "secret_type": "hashicorp_vault_root_service_token", + "display_name": "Hashicorp Vault Root Service Token", + "is_enabled": false + }, + { + "secret_type": "hashicorp_vault_service_token", + "display_name": "Hashicorp Vault Service Token", + "is_enabled": false + }, + { + "secret_type": "terraform_api_token", + "display_name": "Terraform Api Token", + "is_enabled": false + } + ], + "Highnote": [ + { + "secret_type": "highnote_rk_live_key", + "display_name": "Highnote Rk Live Key", + "is_enabled": false + }, + { + "secret_type": "highnote_rk_test_key", + "display_name": "Highnote Rk Test Key", + "is_enabled": false + }, + { + "secret_type": "highnote_sk_live_key", + "display_name": "Highnote Sk Live Key", + "is_enabled": false + }, + { + "secret_type": "highnote_sk_test_key", + "display_name": "Highnote Sk Test Key", + "is_enabled": false + } + ], + "HOP": [ + { + "secret_type": "hop_bearer", + "display_name": "Hop Bearer", + "is_enabled": false + }, + { + "secret_type": "hop_pat", + "display_name": "Hop Pat", + "is_enabled": false + }, + { + "secret_type": "hop_ptk", + "display_name": "Hop Ptk", + "is_enabled": false + } + ], + "Hubspot": [ + { + "secret_type": "hubspot_api_key", + "display_name": "Hubspot Api Key", + "is_enabled": false + }, + { + "secret_type": "hubspot_personal_access_key", + "display_name": "Hubspot Personal Access Key", + "is_enabled": false + }, + { + "secret_type": "hubspot_smtp_credential", + "display_name": "Hubspot Smtp Credential", + "is_enabled": false + } + ], + "Hugging Face": [ + { + "secret_type": "hf_org_api_key", + "display_name": "Hf Org Api Key", + "is_enabled": false + }, + { + "secret_type": "hf_user_access_token", + "display_name": "Hf User Access Token", + "is_enabled": false + } + ], + "IBM": [ + { + "secret_type": "ibm_cloud_iam_key", + "display_name": "Ibm Cloud Iam Key", + "is_enabled": false + }, + { + "secret_type": "ibm_softlayer_api_key", + "display_name": "Ibm Softlayer Api Key", + "is_enabled": false + } + ], + "Intercom": [ + { + "secret_type": "intercom_access_token", + "display_name": "Intercom Access Token", + "is_enabled": false + } + ], + "Ionic": [ + { + "secret_type": "ionic_personal_access_token", + "display_name": "Ionic Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "ionic_refresh_token", + "display_name": "Ionic Refresh Token", + "is_enabled": false + } + ], + "JFrog": [ + { + "secret_type": "jfrog_platform_access_token", + "display_name": "Jfrog Platform Access Token", + "is_enabled": false + }, + { + "secret_type": "jfrog_platform_api_key", + "display_name": "Jfrog Platform Api Key", + "is_enabled": false + }, + { + "secret_type": "jfrog_platform_reference_token", + "display_name": "Jfrog Platform Reference Token", + "is_enabled": false + } + ], + "LaunchDarkly": [ + { + "secret_type": "launchdarkly_access_token", + "display_name": "Launchdarkly Access Token", + "is_enabled": false + } + ], + "Lightspeed": [ + { + "secret_type": "lightspeed_xs_pat", + "display_name": "Lightspeed Xs Pat", + "is_enabled": false + } + ], + "Linear": [ + { + "secret_type": "linear_api_key", + "display_name": "Linear Api Key", + "is_enabled": false + }, + { + "secret_type": "linear_oauth_access_token", + "display_name": "Linear Oauth Access Token", + "is_enabled": false + } + ], + "Lob": [ + { + "secret_type": "lob_live_api_key", + "display_name": "Lob Live Api Key", + "is_enabled": false + }, + { + "secret_type": "lob_test_api_key", + "display_name": "Lob Test Api Key", + "is_enabled": false + } + ], + "Localstack": [ + { + "secret_type": "localstack_api_key", + "display_name": "Localstack Api Key", + "is_enabled": false + } + ], + "LogicMonitor": [ + { + "secret_type": "logicmonitor_bearer_token", + "display_name": "Logicmonitor Bearer Token", + "is_enabled": false + }, + { + "secret_type": "logicmonitor_lmv1_access_key", + "display_name": "Logicmonitor Lmv1 Access Key", + "is_enabled": false + } + ], + "Mailchimp": [ + { + "secret_type": "mailchimp_api_key", + "display_name": "Mailchimp Api Key", + "is_enabled": false + }, + { + "secret_type": "mandrill_api_key", + "display_name": "Mandrill Api Key", + "is_enabled": false + } + ], + "Mailgun": [ + { + "secret_type": "mailgun_api_key", + "display_name": "Mailgun Api Key", + "is_enabled": false + }, + { + "secret_type": "mailgun_smtp_credential", + "display_name": "Mailgun Smtp Credential", + "is_enabled": false + } + ], + "Mapbox": [ + { + "secret_type": "mapbox_secret_access_token", + "display_name": "Mapbox Secret Access Token", + "is_enabled": false + } + ], + "MaxMind": [ + { + "secret_type": "maxmind_license_key", + "display_name": "Maxmind License Key", + "is_enabled": false + } + ], + "Mercury": [ + { + "secret_type": "mercury_non_production_api_token", + "display_name": "Mercury Non Production Api Token", + "is_enabled": false + }, + { + "secret_type": "mercury_production_api_token", + "display_name": "Mercury Production Api Token", + "is_enabled": false + } + ], + "Mergify": [ + { + "secret_type": "mergify_application_key", + "display_name": "Mergify Application Key", + "is_enabled": false + } + ], + "MessageBird": [ + { + "secret_type": "messagebird_api_key", + "display_name": "Messagebird Api Key", + "is_enabled": false + } + ], + "Microsoft Teams": [ + { + "secret_type": "microsoft_teams_incoming_webhook_url", + "display_name": "Microsoft Teams Incoming Webhook Url", + "is_enabled": false + } + ], + "Midtrans": [ + { + "secret_type": "midtrans_production_server_key", + "display_name": "Midtrans Production Server Key", + "is_enabled": false + }, + { + "secret_type": "midtrans_sandbox_server_key", + "display_name": "Midtrans Sandbox Server Key", + "is_enabled": false + } + ], + "MongoDB": [ + { + "secret_type": "mongodb_connectionstring", + "display_name": "Mongodb Connectionstring", + "is_enabled": false + } + ], + "New Relic": [ + { + "secret_type": "new_relic_insights_query_key", + "display_name": "New Relic Insights Query Key", + "is_enabled": false + }, + { + "secret_type": "new_relic_license_key", + "display_name": "New Relic License Key", + "is_enabled": false + }, + { + "secret_type": "new_relic_personal_api_key", + "display_name": "New Relic Personal Api Key", + "is_enabled": false + }, + { + "secret_type": "new_relic_rest_api_key", + "display_name": "New Relic Rest Api Key", + "is_enabled": false + } + ], + "Notion": [ + { + "secret_type": "notion_integration_token", + "display_name": "Notion Integration Token", + "is_enabled": false + }, + { + "secret_type": "notion_oauth_client_secret", + "display_name": "Notion Oauth Client Secret", + "is_enabled": false + } + ], + "NPM": [ + { + "secret_type": "npm_access_token", + "display_name": "NPM Access Token", + "is_enabled": true + } + ], + "NuGet": [ + { + "secret_type": "nuget_api_key", + "display_name": "Nuget Api Key", + "is_enabled": false + } + ], + "Octopus Deploy": [ + { + "secret_type": "octopus_deploy_api_key", + "display_name": "Octopus Deploy Api Key", + "is_enabled": false + } + ], + "Oculus": [ + { + "secret_type": "oculus_access_token", + "display_name": "Oculus Access Token", + "is_enabled": false + } + ], + "OneChronos": [ + { + "secret_type": "onechronos_api_key", + "display_name": "Onechronos Api Key", + "is_enabled": false + }, + { + "secret_type": "onechronos_eb_api_key", + "display_name": "Onechronos Eb Api Key", + "is_enabled": false + }, + { + "secret_type": "onechronos_eb_encryption_key", + "display_name": "Onechronos Eb Encryption Key", + "is_enabled": false + }, + { + "secret_type": "onechronos_oauth_token", + "display_name": "Onechronos Oauth Token", + "is_enabled": false + }, + { + "secret_type": "onechronos_refresh_token", + "display_name": "Onechronos Refresh Token", + "is_enabled": false + } + ], + "Onfido": [ + { + "secret_type": "onfido_live_api_token", + "display_name": "Onfido Live Api Token", + "is_enabled": false + }, + { + "secret_type": "onfido_sandbox_api_token", + "display_name": "Onfido Sandbox Api Token", + "is_enabled": false + } + ], + "OpenAI": [ + { + "secret_type": "openai_api_key", + "display_name": "Openai Api Key", + "is_enabled": false + } + ], + "Orbit": [ + { + "secret_type": "orbit_api_token", + "display_name": "Orbit Api Token", + "is_enabled": false + } + ], + "PagerDuty": [ + { + "secret_type": "pagerduty_oauth_secret", + "display_name": "Pagerduty Oauth Secret", + "is_enabled": false + }, + { + "secret_type": "pagerduty_oauth_token", + "display_name": "Pagerduty Oauth Token", + "is_enabled": false + }, + { + "secret_type": "pagerduty_api_key", + "display_name": "Pagerduty Api Key", + "is_enabled": false + } + ], + "Palantir": [ + { + "secret_type": "palantir_jwt", + "display_name": "Palantir Jwt", + "is_enabled": false + } + ], + "Persona Identities": [ + { + "secret_type": "persona_production_api_key", + "display_name": "Persona Production Api Key", + "is_enabled": false + }, + { + "secret_type": "persona_sandbox_api_key", + "display_name": "Persona Sandbox Api Key", + "is_enabled": false + } + ], + "Pinterest": [ + { + "secret_type": "pinterest_access_token", + "display_name": "Pinterest Access Token", + "is_enabled": false + }, + { + "secret_type": "pinterest_refresh_token", + "display_name": "Pinterest Refresh Token", + "is_enabled": false + } + ], + "PlanetScale": [ + { + "secret_type": "planetscale_database_password", + "display_name": "Planetscale Database Password", + "is_enabled": false + }, + { + "secret_type": "planetscale_oauth_token", + "display_name": "Planetscale Oauth Token", + "is_enabled": false + }, + { + "secret_type": "planetscale_service_token", + "display_name": "Planetscale Service Token", + "is_enabled": false + } + ], + "Plivo": [ + { + "secret_type": "plivo_auth_id", + "display_name": "Plivo Auth Id", + "is_enabled": false + }, + { + "secret_type": "plivo_auth_token", + "display_name": "Plivo Auth Token", + "is_enabled": false + } + ], + "Postman": [ + { + "secret_type": "postman_api_key", + "display_name": "Postman Api Key", + "is_enabled": false + }, + { + "secret_type": "postman_collection_key", + "display_name": "Postman Collection Key", + "is_enabled": false + } + ], + "Prefect": [ + { + "secret_type": "prefect_server_api_key", + "display_name": "Prefect Server Api Key", + "is_enabled": false + }, + { + "secret_type": "prefect_user_api_key", + "display_name": "Prefect User Api Key", + "is_enabled": false + } + ], + "Proctorio": [ + { + "secret_type": "proctorio_consumer_key", + "display_name": "Proctorio Consumer Key", + "is_enabled": false + }, + { + "secret_type": "proctorio_linkage_key", + "display_name": "Proctorio Linkage Key", + "is_enabled": false + }, + { + "secret_type": "proctorio_registration_key", + "display_name": "Proctorio Registration Key", + "is_enabled": false + }, + { + "secret_type": "proctorio_secret_key", + "display_name": "Proctorio Secret Key", + "is_enabled": false + } + ], + "Pulumi": [ + { + "secret_type": "pulumi_access_token", + "display_name": "Pulumi Access Token", + "is_enabled": false + } + ], + "PyPI": [ + { + "secret_type": "pypi_api_token", + "display_name": "Pypi Api Token", + "is_enabled": false + } + ], + "ReadMe": [ + { + "secret_type": "readmeio_api_access_token", + "display_name": "Readmeio Api Access Token", + "is_enabled": false + } + ], + "redirect.pizza": [ + { + "secret_type": "redirect_pizza_api_token", + "display_name": "Redirect Pizza Api Token", + "is_enabled": false + } + ], + "Replicate": [ + { + "secret_type": "replicate_api_token", + "display_name": "Replicate Api Token", + "is_enabled": false + } + ], + "Rootly": [ + { + "secret_type": "rootly_api_key", + "display_name": "Rootly Api Key", + "is_enabled": false + } + ], + "RubyGems": [ + { + "secret_type": "rubygems_api_key", + "display_name": "Rubygems Api Key", + "is_enabled": false + } + ], + "Samsara": [ + { + "secret_type": "samsara_api_token", + "display_name": "Samsara Api Token", + "is_enabled": false + }, + { + "secret_type": "samsara_oauth_access_token", + "display_name": "Samsara Oauth Access Token", + "is_enabled": false + } + ], + "Segment": [ + { + "secret_type": "segment_public_api_token", + "display_name": "Segment Public Api Token", + "is_enabled": false + } + ], + "SendGrid": [ + { + "secret_type": "sendgrid_api_key", + "display_name": "Sendgrid Api Key", + "is_enabled": false + } + ], + "Sendinblue": [ + { + "secret_type": "sendinblue_api_key", + "display_name": "Sendinblue Api Key", + "is_enabled": false + }, + { + "secret_type": "sendinblue_smtp_key", + "display_name": "Sendinblue Smtp Key", + "is_enabled": false + } + ], + "Sentry": [ + { + "secret_type": "sentry_auth_token", + "display_name": "Sentry Auth Token", + "is_enabled": false + } + ], + "Shippo": [ + { + "secret_type": "shippo_live_api_token", + "display_name": "Shippo Live Api Token", + "is_enabled": false + }, + { + "secret_type": "shippo_test_api_token", + "display_name": "Shippo Test Api Token", + "is_enabled": false + } + ], + "Shopee": [ + { + "secret_type": "shopee_open_platform_partner_key", + "display_name": "Shopee Open Platform Partner Key", + "is_enabled": false + } + ], + "Shopify": [ + { + "secret_type": "shopify_access_token", + "display_name": "Shopify Access Token", + "is_enabled": false + }, + { + "secret_type": "shopify_app_client_credentials", + "display_name": "Shopify App Client Credentials", + "is_enabled": false + }, + { + "secret_type": "shopify_app_client_secret", + "display_name": "Shopify App Client Secret", + "is_enabled": false + }, + { + "secret_type": "shopify_app_shared_secret", + "display_name": "Shopify App Shared Secret", + "is_enabled": false + }, + { + "secret_type": "shopify_custom_app_access_token", + "display_name": "Shopify Custom App Access Token", + "is_enabled": false + }, + { + "secret_type": "shopify_marketplace_token", + "display_name": "Shopify Marketplace Token", + "is_enabled": false + }, + { + "secret_type": "shopify_merchant_token", + "display_name": "Shopify Merchant Token", + "is_enabled": false + }, + { + "secret_type": "shopify_partner_api_token", + "display_name": "Shopify Partner Api Token", + "is_enabled": false + }, + { + "secret_type": "shopify_private_app_password", + "display_name": "Shopify Private App Password", + "is_enabled": false + } + ], + "Siemens": [ + { + "secret_type": "siemens_api_token", + "display_name": "Siemens Api Token", + "is_enabled": false + } + ], + "Slack": [ + { + "secret_type": "slack_api_token", + "display_name": "Slack Api Token", + "is_enabled": false + }, + { + "secret_type": "slack_incoming_webhook_url", + "display_name": "Slack Incoming Webhook Url", + "is_enabled": false + }, + { + "secret_type": "slack_workflow_webhook_url", + "display_name": "Slack Workflow Webhook Url", + "is_enabled": false + } + ], + "Snyk": [ + { + "secret_type": "snyk_auth_key", + "display_name": "Snyk Auth Key", + "is_enabled": true + } + ], + "SonarCloud": [ + { + "secret_type": "sonarcloud_token", + "display_name": "Sonarcloud Token", + "is_enabled": true + } + ], + "Square": [ + { + "secret_type": "square_access_token", + "display_name": "Square Access Token", + "is_enabled": false + }, + { + "secret_type": "square_production_application_secret", + "display_name": "Square Production Application Secret", + "is_enabled": false + }, + { + "secret_type": "square_sandbox_application_secret", + "display_name": "Square Sandbox Application Secret", + "is_enabled": false + } + ], + "SSLMate": [ + { + "secret_type": "sslmate_api_key", + "display_name": "Sslmate Api Key", + "is_enabled": false + }, + { + "secret_type": "sslmate_cluster_secret", + "display_name": "Sslmate Cluster Secret", + "is_enabled": false + } + ], + "Stripe": [ + { + "secret_type": "stripe_api_key", + "display_name": "Stripe Api Key", + "is_enabled": false + }, + { + "secret_type": "stripe_legacy_api_key", + "display_name": "Stripe Legacy Api Key", + "is_enabled": false + }, + { + "secret_type": "stripe_live_restricted_key", + "display_name": "Stripe Live Restricted Key", + "is_enabled": false + }, + { + "secret_type": "stripe_test_restricted_key", + "display_name": "Stripe Test Restricted Key", + "is_enabled": false + }, + { + "secret_type": "stripe_test_secret_key", + "display_name": "Stripe Test Secret Key", + "is_enabled": false + }, + { + "secret_type": "stripe_webhook_signing_secret", + "display_name": "Stripe Webhook Signing Secret", + "is_enabled": false + } + ], + "Supabase": [ + { + "secret_type": "supabase_service_key", + "display_name": "Supabase Service Key", + "is_enabled": false + } + ], + "Tableau": [ + { + "secret_type": "tableau_personal_access_token", + "display_name": "Tableau Personal Access Token", + "is_enabled": false + } + ], + "Telegram": [ + { + "secret_type": "telegram_bot_token", + "display_name": "Telegram Bot Token", + "is_enabled": false + } + ], + "Telnyx": [ + { + "secret_type": "telnyx_api_v2_key", + "display_name": "Telnyx Api V2 Key", + "is_enabled": false + } + ], + "Tencent": [ + { + "secret_type": "tencent_cloud_secret_id", + "display_name": "Tencent Cloud Secret Id", + "is_enabled": false + }, + { + "secret_type": "tencent_wechat_api_app_id", + "display_name": "Tencent Wechat Api App Id", + "is_enabled": false + } + ], + "Thunderstore": [ + { + "secret_type": "thunderstore_io_api_token", + "display_name": "Thunderstore Io Api Token", + "is_enabled": false + } + ], + "Twilio": [ + { + "secret_type": "twilio_access_token", + "display_name": "Twilio Access Token", + "is_enabled": false + }, + { + "secret_type": "twilio_account_sid", + "display_name": "Twilio Account Sid", + "is_enabled": false + }, + { + "secret_type": "twilio_api_key", + "display_name": "Twilio Api Key", + "is_enabled": false + } + ], + "Typeform": [ + { + "secret_type": "typeform_personal_access_token", + "display_name": "Typeform Personal Access Token", + "is_enabled": false + } + ], + "Uniwise": [ + { + "secret_type": "wiseflow_api_key", + "display_name": "Wiseflow Api Key", + "is_enabled": false + } + ], + "Unkey": [ + { + "secret_type": "unkey_root_key", + "display_name": "Unkey Root Key", + "is_enabled": false + } + ], + "VolcEngine": [ + { + "secret_type": "volcengine_access_key_id", + "display_name": "Volcengine Access Key Id", + "is_enabled": false + } + ], + "Wakatime": [ + { + "secret_type": "wakatime_api_key", + "display_name": "Wakatime Api Key", + "is_enabled": false + }, + { + "secret_type": "wakatime_app_secret", + "display_name": "Wakatime App Secret", + "is_enabled": false + }, + { + "secret_type": "wakatime_oauth_access_token", + "display_name": "Wakatime Oauth Access Token", + "is_enabled": false + }, + { + "secret_type": "wakatime_oauth_refresh_token", + "display_name": "Wakatime Oauth Refresh Token", + "is_enabled": false + } + ], + "Workato": [ + { + "secret_type": "workato_developer_api_token", + "display_name": "Workato Developer Api Token", + "is_enabled": false + } + ], + "WorkOS": [ + { + "secret_type": "workos_production_api_key", + "display_name": "Workos Production Api Key", + "is_enabled": false + }, + { + "secret_type": "workos_staging_api_key", + "display_name": "Workos Staging Api Key", + "is_enabled": false + } + ], + "Yandex": [ + { + "secret_type": "yandex_cloud_api_key", + "display_name": "Yandex Cloud Api Key", + "is_enabled": false + }, + { + "secret_type": "yandex_cloud_iam_access_secret", + "display_name": "Yandex Cloud Iam Access Secret", + "is_enabled": false + }, + { + "secret_type": "yandex_cloud_iam_cookie", + "display_name": "Yandex Cloud Iam Cookie", + "is_enabled": false + }, + { + "secret_type": "yandex_cloud_iam_token", + "display_name": "Yandex Cloud Iam Token", + "is_enabled": false + }, + { + "secret_type": "yandex_cloud_smartcaptcha_server_key", + "display_name": "Yandex Cloud Smartcaptcha Server Key", + "is_enabled": false + }, + { + "secret_type": "yandex_dictionary_api_key", + "display_name": "Yandex Dictionary Api Key", + "is_enabled": false + }, + { + "secret_type": "yandex_passport_oauth_token", + "display_name": "Yandex Passport Oauth Token", + "is_enabled": false + }, + { + "secret_type": "yandex_predictor_api_key", + "display_name": "Yandex Predictor Api Key", + "is_enabled": false + }, + { + "secret_type": "yandex_translate_api_key", + "display_name": "Yandex Translate Api Key", + "is_enabled": false + } + ], + "Zuplo": [ + { + "secret_type": "zuplo_consumer_api_key", + "display_name": "Zuplo Consumer Api Key", + "is_enabled": false + } + ] +} \ No newline at end of file From 2e78d1cb58e702da373a4dde5fc7a85d6d9a2bd7 Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Sun, 22 Sep 2024 00:34:44 +0530 Subject: [PATCH 002/106] feat(wiki)!: base set of wikipage --- wiki/.eslintrc.json | 3 + wiki/.gitignore | 36 ++++++++++++ wiki/.gitkeep | 0 wiki/README.md | 36 ++++++++++++ wiki/app/favicon.ico | Bin 0 -> 25931 bytes wiki/app/fonts/GeistMonoVF.woff | Bin 0 -> 67864 bytes wiki/app/fonts/GeistVF.woff | Bin 0 -> 66268 bytes wiki/app/globals.css | 27 +++++++++ wiki/app/layout.tsx | 35 +++++++++++ wiki/app/page.tsx | 101 ++++++++++++++++++++++++++++++++ wiki/next.config.mjs | 4 ++ wiki/package.json | 26 ++++++++ wiki/postcss.config.mjs | 8 +++ wiki/tailwind.config.ts | 19 ++++++ wiki/tsconfig.json | 26 ++++++++ 15 files changed, 321 insertions(+) create mode 100644 wiki/.eslintrc.json create mode 100644 wiki/.gitignore delete mode 100644 wiki/.gitkeep create mode 100644 wiki/README.md create mode 100644 wiki/app/favicon.ico create mode 100644 wiki/app/fonts/GeistMonoVF.woff create mode 100644 wiki/app/fonts/GeistVF.woff create mode 100644 wiki/app/globals.css create mode 100644 wiki/app/layout.tsx create mode 100644 wiki/app/page.tsx create mode 100644 wiki/next.config.mjs create mode 100644 wiki/package.json create mode 100644 wiki/postcss.config.mjs create mode 100644 wiki/tailwind.config.ts create mode 100644 wiki/tsconfig.json diff --git a/wiki/.eslintrc.json b/wiki/.eslintrc.json new file mode 100644 index 0000000..3722418 --- /dev/null +++ b/wiki/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/wiki/.gitignore b/wiki/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/wiki/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/wiki/.gitkeep b/wiki/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/wiki/README.md b/wiki/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/wiki/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/wiki/app/favicon.ico b/wiki/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/wiki/app/fonts/GeistMonoVF.woff b/wiki/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000000000000000000000000000000000000..f2ae185cbfd16946a534d819e9eb03924abbcc49 GIT binary patch literal 67864 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ϣ%UY8`RWp~V-U_7 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 CLkb=M literal 0 HcmV?d00001 diff --git a/wiki/app/fonts/GeistVF.woff b/wiki/app/fonts/GeistVF.woff new file mode 100644 index 0000000000000000000000000000000000000000..1b62daacff96dad6584e71cd962051b82957c313 GIT binary patch literal 66268 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 >-Aun5A4Gp>4U0IQJ zJSDj%uq;_-j;8!z8*BN3#G5`ojMF>mZtK$CmJZ>LZBP#+{!QxI(n!6=j?D+5s8yl| zCqq%@Li|olF66yc&uRtqxK_{9<1Bz%WM|3)$GtRZvu6gM<72a@tfd#+V6(pWfBD**uQxR;owP8FIttM>^4T=+ zFYN&$EludBGthdY*q;-P4l)cZvz=S2KfBDRiZdk$T!jv@&mB^%V^Q1_xXKs?qV=+O z7JK9WX_6hj5rQ5#_#XZR<>aHdT&e4ifAZwWse0~aHapMWG&cBWv{?RZ`hEHB@_nuF zy}fbqt#tNX)bur{>6ftehFiZkNd>Ryw`lrJv#{N3PTAXz)`CuJPCB~geMIozQlm#$5l!D;X zfUQ1!IFD;IjI^b*Mkgk>MUhTnv4a>qY7RRms)c0?WH-vw-S9;aXwyNe7Ta*5``;;g^I(Vd`+I0u7da=e}#F;{J_6W$C;2b`UBI+E~4_A_HQQ5 zEQ&p-|FvZ}rahkr&RN0U9c#S3P4p`5%G$~Q1Gow$7~C7M`U(n zH^FiFC6R_ryR#`dH%S4ZDE#M*I!7-^?m}M>oyQ08|KKpz^j+15&QmYy$Q`n%QO3zYhIp< zL@=uru9zHQ&p+^Mf`TE$N6+X3DXHLFHM7ULndU-NzDCgbzO@DRYM`}{g9Ucx2d0wT zg|vXtmgY(G{#9P|@KChWPlr8W`g(H1hNk~a>J&0B02gHsTNjj>*_i%Cgna)s>-q)} zxaIxqdlH*u{aqw9fqCww89ikAvHf?Q$#we#8Dn1}a=W$}OpqPy5^-&9Avuoir=($k?pgH2#cR*9FeVS_gLRc7U0k+2y92<1`CP zAP|x#R&QbPF}jnpTfaTSa3cH#v3D)=rS=>G23m#FFV*t7k4bvAKuVE8{3!#`2WN3wo)f6L0KwAkO>ECG`!KDm9U&Aj#-xeF?-Sk^#N4MY2 zU*K+D^9rFIH3hnht<#=H3WI*w_w%358;ibQ@gDcbe2?DO{khi%(YMbMP~(*oqXD#| zcd^%2_HY!2T)|3<7?dgI2@9=B zrQ>K)@X=?cYYwfUkafI;oV=Cl_)4^L)F~LK{e60f@)nUL_9PX7=P} z4(!MF^v4eT3Q6*RSm+w(M0qf7p-4!W{W=i;s*Nsw$amYf+IzTPq>erZZ$br>9Ku&G# zQ>k{y#@X0ocWW8vySn!eNXe`O3Y%_3`aNctsL8LKLf? z?6Zw>jM~rIAuZvY#F}!9x!2wyPHmY$t9Fb&-`GKKZtd5(a>#|`JwQMTK7EN7xJCFH z?SA3--bMO8tizXeA7jb64@jMGRAQ`)dyb1xr!5igNHU={3!alyt;=AmJY-u{FksRd zKX>P|+llT7=eS4T8e4a7uDcqQW855ncNZYo3G@y_xJTk2gJ92)L&;q2Qw7vz<6RhI zw69j=^56RYvX6_shj#K6oiw|&A4v9{sZgJ$*|?6mI630@V9j*%BPhV#=cM2qrIK|D zX~^2=#b_BJqjw6f(B9|fXc@G*vQPEeI0i=Wm_W(7i#qPuA#2z`m8LZXr_mU+T&hip zwl-wZS{Y*pGz4Z}7;?O?OauSAbKuX!kzq>kN!N}2zjcsT{WY;-f&2fqYxuuLt!}); zzFGn$l7;uW0FrtCtIWI(Z~-)N;#jTou6vwTdnnBt`K1nSXBWmDFf<|}SXlju8GT7c zDzz2vK5<9i|zx4aAwo>ml>7lgPd0s?QLl96URHi1yXy{%tO~s zB1rNfQ*OVcj6eJ36ND}6NeSvvnD7AKoH&5?A)dpd(bEr_K-F`5po-tN#zPiNm{fog zdTEAB$lHrs zvw2rdi&jvE*CC3{axexwRt7rIAKxW_`XF@}WU&<5Z!0Wu;|bkB=ic3t$g&s+{2=$K z31U7BBzu;|A(UkB{WVO#wKG;tPY!tm5^&I1j@<`TW zkOVQAZ7Fn3%tLi74>1hKdVCHA_siV;g=!pmqjfY@GpjhDBI`Ay&i(cDCaAr;sNF}{ z_kj!Uu;)iyu9|=&`(2GdpWSTTKSM@R6& z_?=updf73kQ0!e#x@RSg&bHodW%ofewxmL3UKv zTMJ+1vpAkWpANd$2jXtUM&UExm{Z0s*l-=Y=Amon3s0XrKTWp64IaR6*IF*$ZlUF& zIa$HMA-IAs1;!zJvsLuuvRVDy=Ijm$-`+)cj)UC@f1XM8eW_21cZw$=l-n&w$;qW9 zw`=bbZ=$nvGk%9hwTpl&c2mBe(xewGT=s0(E3A&8b1SOyS+$zk1YstbRUOg4qAl?> zwUCFwW8|FHZyoTgmud9>M}*D2IgOi#rM=uE;hQPB(l6b)Wm13d4|wPgP?H;qBq1JD zF-T_-*oR@T#)eJ+)A2>XeCadW_4;=!b4G?0~@LZY}0}fduLs=7p)>B0refS&IQ9HKyv$5Pm zG2O=VfCUAZ~&T8i~ub~MczSu)OH0Fc$8 zf#Fc77^^Tg=?-zqya)SOEr4lvciFmRh*NhwJEDl@WZI6vSQo#5X=lF}2BaMt?@+-P zEZ?dxju%+o4;6=74l={_n9x4T5I8M&UM+WK1uU2NU{7;60+}QrnOR9Ut41MqZpz>p zh46foHsXHtJm>WQTrDzft)Mw3m;$6GosoWZGT41ae13Au)u$Y(VOHATaIkeC(3Q&h z>VcPSZj`Mn;h^HXguh5)NH}XsFdQVdb%#_A_OYu;LNZ&5?Ckc5_S}UrpoM7W9e5G{H zH+LUjKRzIQpdf#+d{>tE85lf@s0+&|psOfF4I-zv&4ue#K$t&4(^&sDu= zpkFh5ae=>o9qEGs20d`c@@}}I`WHt+Y*%OaV)k!@w9a^Ccff>gYVJu5nGLi0%Eaxl z&4@=evMRjrkBM^cx%8ev=mjNp(JM5@4%^i1gWr<1!#UL)ny%Qi14)}Khz>lf)f)cd z#7#$U1fU)wQgLlm_!2yy^Y?&;-4P-XPYLlBela3c2=tLy#@u4wd1MVQ=I%fT@s284 z%HFf)FPIh|;ZB!vP2Y>(f-n$HMRt^yq`E^xYjjtBQP&WEbmPq>zVN&dnc(NpMgL^q zza9tZX=1W}Jsz233Ho}iweZR5Q^J14W3NT*V z&7`Y7z^4H(?Xq-rifx^#A)EE5_)J=zO1N~}z2}3DO}ps{3MJ=d-9>`_W&!#6&Sj7F zamHoZs_&S!*u>A%ER(KDhZ?|G0MFsW4r)OZS*@P^qaRDCoN`Ex;TKsANj{RI|6>|` zri8nBpAJfnX&-F5{c=#rif)dOs}Tq1g{%_YXthK!-KoV z{6mExa$bu*P!#;cn?y@l3HKMdUzfn0>5OpwCm8Flit9&qnU7EHQG42)JnmZ)(zdWQ zn(qC5G;*-r2sZ2VE3R9B3eUidt$(JwOhtd>EaX+O;n*OUqW^3hEz;-V`1~9Zv$3Z%2oX{`zyV*ZFoG#P_kv`siRF*W_g!otEmF)`6%U>cM7b8UK*-Ic(t z`NMNiU0vfG+qKR*&yr!`h07%UrAhyX(&mcoIsJVS^yrV@Ca-mQX0>S)mQ`^YmT7VN zVNGJu5!*d?QR^@Oq7m{9lq9WJQ=dWZ7X1e821ESUNV+1IoAMQED_lLg$z&KGl9z-n zXjxeRkdZVlf{b{?pL03 zQ*!BF198koVI*OzF)zBmeO)epNeN`$ehx6+x~2KsXLort#=Fk_;g+O$FQnKk3Vlf7 zpVNa_dGCm7c(zZcRWiw#sCP3>XMi;hr%gPp7gRm_eyvP|uUB9nRb3@tHwnE+>U8Yc zQaaS|a!X1*F!2!4Oyvcvu*rP1d}kt!5YAta^C7!oG+DQFmP*Ee*QJ zJQ8EpEHes3HOfI4kFJ7q|x*TFy`wax^-(b+5A`^^82E0<*bsX z-j?}yIXsACCY5AP8IotnI~TsiYU5&4emqafJZnP=H#V198~1Z7`w$g}Gp}fC_BcUB z*7?Wim_qy6UW32J82DI$|LWNGdltd94axExv&+@uL`aY0p;UIaU~AUfGVp!Uv?4vw z(U(>B)^E7*ZBhPwJ9Gjg!zQDGIpz?HA=GlhgBKc&<=W~cvU=t^VwXoBLD>#BSu{E| zi}a)h@p0GgMj0!IDnJWLXTk?QSu_9CWYcH*hKY2qJo-M$fnp3TwLQL>!Xg9OtDbE> za8=rqhm?}bo5;fv zU0{?;@sFUQ1PrMZeO!p*P=~=*T;{=1N1ME2@D|MVWTF15zQ`h3uU4g?Ua(ZM@b2X9 zhaZhP9~vZ1fJ%#Zi)O7+OUCDi9SnNFeC1A1p=$6rq#M3kDWf~*i=esSP2fHZU2X2} zcpt}y9*i&Ahsgfqm-l|2c*a<8HH=Q&AGhF)&@*(U;SOkz2Fdapo!v8vQjZoRQM3@T zqVXxE<0h6yewonzhCZn;fmJSiwUc1wiz&agR;S@@0e0Jo(c8jij7?lVZN=bRnC`vg z=W-Lpm&6-4DiOV#@}JfU5a*ph-fW|`4lbXbm_39hP$`0Ud^oSZ#aASh<98CzeYE6r zh;WO-kf0DZmIiJCMn8|VEe3(t`eIJW6e zY}1hXwPkhS7-KH$vwZzo-IO0>^d3zI8biH(%6x5~j)xLs`UK8Rl?$2`F1l7DnxTY} zmXsEJXVc?*_@{bOXl!$#1`b!XOKN>V{3km}0>_rb@Cz7!?ucFLSfMPouHnk?x5wUL zX`VGNw;3^UD{SA=kHc|@6rB|yC3!;OrEcGWv4VtHI4g@4##`+w*xX9GusX_`xyUMt zksR|DcXpM>h)#JBGx7gaPl27M-IB+8>-ipJQ8Z0?kmH}=Jz5_aiB;(g@dt|d)+3R7 zXsez%aLI`=s>N=J^dQ?5RODWZ{LGz_re&(YJTr+`t3T;}2yLTQtRl_m8sJ`pSs>e4 z?mD>7H#qfXGPGQzqiqhdFcx14^chAee!tQ?Mo0f{)M=QS(jHqIS@aU|I)QiOX6LTl zM*yxN$Ni>eo27sfpQt)5_0rP(*Ew_{oloN*obq~cUA`MVi*=I46*cuU>j#=96SX`> z%rPTz(FA3%xHQnen;k(NwKE61i+;bNV7(K25_td-@Lc-7;;B`ztagmRGkU?+4|z)6 zH|14o%^EEz^JNixm7Z+YkfS)V;d;QR75_9H(*q_b6_9+T)35W|n?m3-Az4=Pa*$U{$1hr^Z!Cz$X*WHAbO6o$&C$H${4HGHkB%MEI*-t zu<6pAo8MY4q}RQ{(O22?Or+GML~y5eIHCi+(PhfX|ES!5Zu+7=O*yDOwPWi&4kPMy z!z}TWVBybuKhr?9=Q43d_@EtP40dv=J)&W|+;s99N%$p1kO4QhxxYL28=E;mp|?0aB56{dI!8UAfElgz zXR#B#DY$T*!>Cnc$e41`L}6%7mEDvUk|pJsIi+hY&`QZlK&+>wB8bh?mV;Z@N&|xX zYs8T-Hqod0mv`l>(n0gVrhDRatwsY3YX#8DK)pjZM&-OJMunYK)v_i|V-*>_Re`C` z<%`mx8=hZrRS2$MPS+I(1ELVf^*^;}U51lwR*>)t(Qo4Ts%6=jc1v5SlyQ*hq6j&< z&x8(3X%8>(%xVA~-X+S_)qC28Ib#Z6*m1@TV4;uStfz!4X-0H6ExaSt7}A%w1Zt?t&Idal)10W>YDZK8p)5W*u2 zFes$Bazzdg7ruNoHD97OIZG&orKig0>xRF}$e&c}9|UaQ{f3iY|i?2RPP(-=l2(!Lp#90zHaE87&$4~*c1q4*!1Bu*t4|Y8^{xm(Y z>@D#Kb1qH8w>t;kLhRf88W!K6P2ZcrAD|a*HihoM$w{F0Ca37Z-AxRMqsDU%bM9`u z^8lMdq-Lat6>seS7Zea@p4DI0D_ijKEmPWFJHKl9^>x3!1~t;yHUhgcv1+1XeBEL@ zot-X;y7Rm}3Mm{!$;3_^s(X-dya@tBm7j(zc`8Hj#+(ynF>Y40;wmbl62XElt(CJE z9z1_kY_8MNLR(aYo;)dSVKKNDOogYwRz+RJQ%;Ru_#pD^bn)#WD~?gvsnQYpDvWSH zihsm$VZdJz`g-wmc4EL^5c)dt9e>?yyBXu5bKQhO=Vje|@5%kVVsyfoer|8l8Y7=~E?%T9 zR@QxP9_@@*Fj{TIw(OEc{j^eHi%_*;RHO4OznSC9VFNn?EcB}y2YeDP1BDft6`K{E z^%o{i9C#RfAbBT^=ij@4aqvUPR7h$ldIDukZQxSM7D0Ijdy#($I}v}1dXxP<_XUZ~ zMQ5zvn3*)u_-NjKKO~z=RmxTN#WvMt@1y5p*F=7k`6_<=9Y`2B8~A~fBBzq+N+rlpH+L46(|$A z3=yHT&`7ZgR<-=JMp^HBTi3_2EwJg30i3FuvH{kX)~5i?mu8`>4z3y5CdaEHuIV}^ z%d0Z3nVTlht3pp{d?wSYQcoG3CfBQCPw74;+pBU*hL=xT1H`xDrldRxI8;$d#B9V< zu2T+EE>ljjF0xLtZc{y+iT6lmT*I8h+`|UA)8N$<_C$Na$E3%`$EaojPH9dpPVr7b zPK8cMPK`>(*5}$6+I!k(+DF<~+Pm5k!qM1eRB56X<>%%yPIv{UKfTvK9Xl^gH^i#j zpiN;8I2WFD$S!QHPGm!{2v@pN=1j)Cu7D|9D|4{SF2c;U!kY6o`>PaU(SlA)=P1f~ zo_#0_NW8AJSLLqATAac*qf^*!%3B&|cWf?#Z_pkmGSphNAHQ#Fimvsp`LroSbH~#! zsGK?fy}eId6KEZU=7nc%R5fsph+|eHF2F6oCBP#i+c3ZPvDe6LBg<1SGG%D?-)6`r zD_t&dGH^0*GjK8R)Ns~t*KpPF*m2tZ+}A!IMJz!9T8AJS;Oz~lS zU#ON1Hn^6NHprGZ#Fn2>SW%p-DQA+l87V8YlXhE|Mmjv(`Ko(}s>c!o+gaN7WR=T| z)zD^VUx(6IRTea3*X0U4gZEYJSVX2J*E81y`XiniRE5tH2I2zccwu{;zq@aA4USu2 zjLhxT+_?Hz=;=N=o>#30?Wx1!oO5ejFsI9=9_bd_eFMYFft6%O4iqg>!ZfQ0)K-Lv z^JM!jVDgQTp9X#rl76h@ikCvVl0ElVqI*1X9l9S&COz@R5c)(@7=>B2T;?uyaX)nL zhWec$K!2K4N}uBl8r#DSJ8GvvP&g)RKcm7Kl@c&!IZ)E&N@Xc=MbC2uvT)ICaQQ$K z3Df}zxi<3&zM-6BPON72w`L8$YWD<;3nZFu`;kS$W6&jf1)KUzkz=L G)cz05(PHWV literal 0 HcmV?d00001 diff --git a/wiki/app/globals.css b/wiki/app/globals.css new file mode 100644 index 0000000..13d40b8 --- /dev/null +++ b/wiki/app/globals.css @@ -0,0 +1,27 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} diff --git a/wiki/app/layout.tsx b/wiki/app/layout.tsx new file mode 100644 index 0000000..a36cde0 --- /dev/null +++ b/wiki/app/layout.tsx @@ -0,0 +1,35 @@ +import type { Metadata } from "next"; +import localFont from "next/font/local"; +import "./globals.css"; + +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/wiki/app/page.tsx b/wiki/app/page.tsx new file mode 100644 index 0000000..433c8aa --- /dev/null +++ b/wiki/app/page.tsx @@ -0,0 +1,101 @@ +import Image from "next/image"; + +export default function Home() { + return ( + + ); +} diff --git a/wiki/next.config.mjs b/wiki/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/wiki/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/wiki/package.json b/wiki/package.json new file mode 100644 index 0000000..82f318e --- /dev/null +++ b/wiki/package.json @@ -0,0 +1,26 @@ +{ + "name": "wiki", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "react": "^18", + "react-dom": "^18", + "next": "14.2.13" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "eslint": "^8", + "eslint-config-next": "14.2.13" + } +} diff --git a/wiki/postcss.config.mjs b/wiki/postcss.config.mjs new file mode 100644 index 0000000..1a69fd2 --- /dev/null +++ b/wiki/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/wiki/tailwind.config.ts b/wiki/tailwind.config.ts new file mode 100644 index 0000000..d43da91 --- /dev/null +++ b/wiki/tailwind.config.ts @@ -0,0 +1,19 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + background: "var(--background)", + foreground: "var(--foreground)", + }, + }, + }, + plugins: [], +}; +export default config; diff --git a/wiki/tsconfig.json b/wiki/tsconfig.json new file mode 100644 index 0000000..e7ff90f --- /dev/null +++ b/wiki/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From 3189f666f5192be23590fd0514d9730eb35e3959 Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Sun, 22 Sep 2024 13:09:49 +0530 Subject: [PATCH 003/106] feat(docker)!: updated dockerfile and publish workflow --- .github/workflows/.gitkeep | 0 .github/workflows/container_publish.yml | 64 +++++++++++++++++++++++++ src/docker/Dockerfile | 19 +++++++- 3 files changed, 82 insertions(+), 1 deletion(-) delete mode 100644 .github/workflows/.gitkeep create mode 100644 .github/workflows/container_publish.yml diff --git a/.github/workflows/.gitkeep b/.github/workflows/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/workflows/container_publish.yml b/.github/workflows/container_publish.yml new file mode 100644 index 0000000..53cc255 --- /dev/null +++ b/.github/workflows/container_publish.yml @@ -0,0 +1,64 @@ +name: Create and publish a Docker image + +# Configures this workflow to run every time a change is pushed to the branch called `release`. +on: + push: + branches: + - 'develop/*' + paths: + - 'src/docker/**' + - 'src/python/**' + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + attestations: write + id-token: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: ./src/docker/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + diff --git a/src/docker/Dockerfile b/src/docker/Dockerfile index 5ab77ef..2b69524 100644 --- a/src/docker/Dockerfile +++ b/src/docker/Dockerfile @@ -1 +1,18 @@ -# Dockerfile \ No newline at end of file +# Use an official Python runtime as a base image +FROM python:3.12-slim + +# Set the working directory inside the container +WORKDIR /app + +# Copy the Python source code and other necessary files into the container +COPY src/python /app + +# Install the Python package from the copied files +RUN pip install --no-cache-dir . + +# Environment variables for paths to your config.ini and tokenManager.json files +ENV CONFIG_PATH=/app/config.ini +ENV TOKEN_PATH=/app/tokenManager.json + +# Set the entry point to the how2validate command +ENTRYPOINT ["how2validate"] \ No newline at end of file From ddaf61ee51e1a00086f6119af463ebb80c042d03 Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Sun, 22 Sep 2024 14:31:19 +0530 Subject: [PATCH 004/106] feat(Py_package)!:updated py package --- .../workflows/{container_publish.yml => docker_publish.yml} | 0 src/python/config.ini | 2 +- src/python/how2validate/validator.py | 6 +++--- src/python/setup.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename .github/workflows/{container_publish.yml => docker_publish.yml} (100%) diff --git a/.github/workflows/container_publish.yml b/.github/workflows/docker_publish.yml similarity index 100% rename from .github/workflows/container_publish.yml rename to .github/workflows/docker_publish.yml diff --git a/src/python/config.ini b/src/python/config.ini index 0980aba..4857e2f 100644 --- a/src/python/config.ini +++ b/src/python/config.ini @@ -1,6 +1,6 @@ [DEFAULT] package_name = how2validate -version = 0.0.0a6 +version = 0.0.0a8 [SECRET] secret_active = Active diff --git a/src/python/how2validate/validator.py b/src/python/how2validate/validator.py index 9aab639..c684a48 100644 --- a/src/python/how2validate/validator.py +++ b/src/python/how2validate/validator.py @@ -2,7 +2,7 @@ import logging from how2validate.utility.config_utility import get_active_secret_status, get_inactive_secret_status, get_package_name, get_version -from how2validate.utility.tool_utility import format_serviceprovider, format_services, get_secretprovider, get_secretservices, redact_secret, update_tool, validate_choice +from how2validate.utility.tool_utility import format_serviceprovider, format_services, format_string, get_secretprovider, get_secretservices, redact_secret, update_tool, validate_choice from how2validate.utility.log_utility import setup_logging from how2validate.handler.validator_handler import validator_handle_service @@ -50,9 +50,9 @@ def parse_arguments(): def validate(provider,service, secret, response, report): logging.info(f"Started validating secret...") - result = validator_handle_service(service, secret, response, report) + result = validator_handle_service(format_string(service), secret, response, report) logging.info(f"{result}") - return f"{result}" + # return f"{result}" def main(args=None): if args is None: diff --git a/src/python/setup.py b/src/python/setup.py index 3250ffa..98feeca 100644 --- a/src/python/setup.py +++ b/src/python/setup.py @@ -30,7 +30,7 @@ packages=find_packages(), include_package_data=True, package_data={ - '': ['*.txt', '../requirements.txt', '../config.ini', '../tokenManager.json', '../README.md', '../main.py'], + '': ['../requirements.txt', '../config.ini', '../tokenManager.json', '../README.md', '../main.py'], }, install_requires=requirements, entry_points={ From 86dde250604303a3c0085c8c2a1e2e9ea83f41ea Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Sun, 22 Sep 2024 16:17:11 +0530 Subject: [PATCH 005/106] feat(jsr)!: updated package jsr --- src/js/.gitkeep | 0 src/js/index.ts | 3 +++ src/js/jsr.json | 6 ++++++ src/js/package.json | 27 +++++++++++++++++++++++++++ 4 files changed, 36 insertions(+) delete mode 100644 src/js/.gitkeep create mode 100644 src/js/index.ts create mode 100644 src/js/jsr.json create mode 100644 src/js/package.json diff --git a/src/js/.gitkeep b/src/js/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/js/index.ts b/src/js/index.ts new file mode 100644 index 0000000..6429788 --- /dev/null +++ b/src/js/index.ts @@ -0,0 +1,3 @@ +export default function hello(){ + console.log("Hello User!") +} \ No newline at end of file diff --git a/src/js/jsr.json b/src/js/jsr.json new file mode 100644 index 0000000..cfa0b74 --- /dev/null +++ b/src/js/jsr.json @@ -0,0 +1,6 @@ +{ + "name": "@how2validate/how2validate", + "version": "0.0.1", + "license": "MIT", + "exports": "./index.ts" + } \ No newline at end of file diff --git a/src/js/package.json b/src/js/package.json new file mode 100644 index 0000000..48aafdf --- /dev/null +++ b/src/js/package.json @@ -0,0 +1,27 @@ +{ + "name": "how2validate", + "version": "0.0.1", + "description": "A cli and package for validating secrets.", + "main": "index.ts", + "scripts": { + "test": "node test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Blackplums/how2validate.git" + }, + "keywords": [ + "secret", + "how2validate", + "secretvalidate" + ], + "author": "Vigneshkna", + "license": "MIT", + "bugs": { + "url": "https://github.com/Blackplums/how2validate/issues" + }, + "homepage": "https://github.com/Blackplums/how2validate#readme", + "dependencies": { + "jsr": "^0.13.2" + } +} From 164649293e5eadd3009b72b9cef097e8fdef4b59 Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Mon, 23 Sep 2024 20:38:26 +0530 Subject: [PATCH 006/106] feat(Js_package):updated js packages --- src/js/README.md | 14 + src/js/config.ini | 7 + .../how2validate/handler/validator_handler.ts | 28 + src/js/how2validate/index.ts | 86 + src/js/how2validate/utility/config_utility.ts | 67 + src/js/how2validate/utility/log_utility.ts | 29 + src/js/how2validate/utility/tool_utility.ts | 165 ++ .../validators/npm/npm_access_token.ts | 57 + .../validators/snyk/snyk_auth_key.ts | 63 + .../validators/sonarcloud/sonarcloud_token.ts | 58 + src/js/index.ts | 3 - src/js/jsr.json | 14 +- src/js/package.json | 23 +- src/js/tokenManager.json | 1799 +++++++++++++++++ src/js/tsconfig.json | 13 + 15 files changed, 2415 insertions(+), 11 deletions(-) create mode 100644 src/js/README.md create mode 100644 src/js/config.ini create mode 100644 src/js/how2validate/handler/validator_handler.ts create mode 100644 src/js/how2validate/index.ts create mode 100644 src/js/how2validate/utility/config_utility.ts create mode 100644 src/js/how2validate/utility/log_utility.ts create mode 100644 src/js/how2validate/utility/tool_utility.ts create mode 100644 src/js/how2validate/validators/npm/npm_access_token.ts create mode 100644 src/js/how2validate/validators/snyk/snyk_auth_key.ts create mode 100644 src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts delete mode 100644 src/js/index.ts create mode 100644 src/js/tokenManager.json create mode 100644 src/js/tsconfig.json diff --git a/src/js/README.md b/src/js/README.md new file mode 100644 index 0000000..9eb8855 --- /dev/null +++ b/src/js/README.md @@ -0,0 +1,14 @@ +# How2Validate + +How2Validate is a package designed to validate secrets and sensitive information across multiple platforms. + +## Features + +- Validate API keys, passwords, and other sensitive information. +- Cross-platform support (Windows, Linux, macOS). +- Easy integration with existing applications. + +## Installation + +```bash +npx jsr add @how2validate/how2validate diff --git a/src/js/config.ini b/src/js/config.ini new file mode 100644 index 0000000..0bcf49c --- /dev/null +++ b/src/js/config.ini @@ -0,0 +1,7 @@ +[DEFAULT] +package_name = @how2validate/how2validate +version = 0.0.1 + +[SECRET] +secret_active = Active +secret_inactive = InActive \ No newline at end of file diff --git a/src/js/how2validate/handler/validator_handler.ts b/src/js/how2validate/handler/validator_handler.ts new file mode 100644 index 0000000..4269912 --- /dev/null +++ b/src/js/how2validate/handler/validator_handler.ts @@ -0,0 +1,28 @@ +import { validateSnykAuthKey } from '../validators/snyk/snyk_auth_key'; +import { validateSonarcloudToken } from '../validators/sonarcloud/sonarcloud_token'; +import { validateNpmAccessToken } from '../validators/npm/npm_access_token'; + +type ValidatorFunction = (service: string, secret: string, response: boolean, report?: boolean) => Promise; + +const serviceHandlers: Record = { + snyk_auth_key: validateSnykAuthKey, + sonarcloud_token: validateSonarcloudToken, + npm_access_token: validateNpmAccessToken, + // Add all your services here +}; + +export async function validatorHandleService( + service: string, + secret: string, + response: boolean, + report?: boolean +): Promise { + // Get the handler function based on the service name + const handler = serviceHandlers[service]; + + if (handler) { + return handler(service, secret, response, report); + } else { + return `Error: No handler for service '${service}'`; + } +} diff --git a/src/js/how2validate/index.ts b/src/js/how2validate/index.ts new file mode 100644 index 0000000..a8157bc --- /dev/null +++ b/src/js/how2validate/index.ts @@ -0,0 +1,86 @@ +import { Command } from 'commander'; +import { setupLogging } from './utility/log_utility'; +import { + getActiveSecretStatus, + getInactiveSecretStatus, + getPackageName, + getVersion +} from './utility/config_utility'; +import { + formatServiceProviders, + formatServices, + getSecretProviders, + getSecretServices, + redactSecret, + updateTool, + validateChoice +} from './utility/tool_utility' +import { validatorHandleService } from './handler/validator_handler'; + + +// Call the logging setup function +setupLogging(); + +const program = new Command(); + +program + .name('How2Validate Tool') + .description('Validate various types of secrets for different services.') + .version(`How2Validate Tool version ${getVersion() as string}`, '-v, --version', 'Expose the version'); + +const providerChoices = getSecretProviders(); +const serviceChoices = getSecretServices(); + +// Define CLI options using commander +program + .option('-provider ', `Secret provider to validate secrets\nSupported providers:\n${formatServiceProviders()}`, (value) => validateChoice(value, providerChoices)) + .option('-service ', `Service / SecretType to validate secrets\nSupported services:\n${formatServices()}`, (value) => validateChoice(value, serviceChoices)) + .option('-secret ', 'Pass the secret to be validated') + .option('-r, --response', `Prints ${getActiveSecretStatus()} / ${getInactiveSecretStatus()} upon validating secrets`) + .option('-report', 'Reports validated secrets over E-mail', false) + .option('--update', 'Hack the tool to the latest version'); + +async function validate(provider: string, service: string, secret: string, response: boolean, report: boolean) { + console.info("Started validating secret..."); + const result = await validatorHandleService(service, secret, response, report); + console.info(result); +} + +async function main() { + program.parse(process.argv); + + // Convert options keys to lowercase for consistency + const options = Object.fromEntries( + Object.entries(program.opts()).map(([key, value]) => [key.toLowerCase(), value]) + ); + + if (options.update) { + try { + console.info("Initiating tool update..."); + await updateTool(); + console.info("Tool updated successfully."); + return; + } catch (error) { + console.error(`Error during tool update: ${error}`); + return; + } + } + + if (!options.provider || !options.service || !options.secret) { + console.error("Missing required arguments: -provider, -service, -secret"); + console.error("Use '-h' or '--help' for tool usage information."); + // program.help(); + return; + } + + try { + console.info(`Initiating validation for service: ${options.service} with secret: ${redactSecret(options.secret)}`); + await validate(options.provider, options.service, options.secret, options.response, options.report); + console.info("Validation completed successfully."); + } catch (error) { + console.error(`An error occurred during validation: ${error}`); + console.log(`Error: ${error}`); + } +} + +main().catch(error => console.error(`Unexpected error: ${error}`)); diff --git a/src/js/how2validate/utility/config_utility.ts b/src/js/how2validate/utility/config_utility.ts new file mode 100644 index 0000000..2665c64 --- /dev/null +++ b/src/js/how2validate/utility/config_utility.ts @@ -0,0 +1,67 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import * as ini from 'ini'; + +interface Config { + DEFAULT?: { + package_name?: string; + version?: string; + }; + SECRET?: { + secret_active?: string; + secret_inactive?: string; + }; +} + +let config: Config | null = null; + +export function initConfig() { + // Path to the config.ini file, relative to where your compiled JavaScript will be running (in dist) + const configFilePath = path.resolve(__dirname, '..', '..', 'config.ini'); // Adjusted for dist directory + + try { + const configContent = fs.readFileSync(configFilePath, 'utf-8'); + config = ini.parse(configContent); // Parse the .ini file + } catch (error) { + console.error(`Error: The file '${configFilePath}' was not found or could not be read.`); + } +} + +// Function to get the package name from the DEFAULT section +export function getPackageName(): string | undefined { + if (config && config.DEFAULT) { + return config.DEFAULT.package_name as string; + } else { + throw new Error("Configuration not initialized. Call initConfig() first."); + } +} + +// Function to get the active secret status from the SECRET section +export function getActiveSecretStatus(): string | undefined { + if (config && config.SECRET) { + return config.SECRET.secret_active as string; + } else { + throw new Error("Configuration not initialized. Call initConfig() first."); + } +} + +// Function to get the inactive secret status from the SECRET section +export function getInactiveSecretStatus(): string | undefined { + if (config && config.SECRET) { + return config.SECRET.secret_inactive as string; + } else { + throw new Error("Configuration not initialized. Call initConfig() first."); + } +} + +// Function to get the version from the DEFAULT section +export function getVersion(): string | undefined { + if (config && config.DEFAULT) { + return config.DEFAULT.version as string; + } else { + throw new Error("Configuration not initialized. Call initConfig() first."); + } +} + +// Call initConfig when this file is imported or used +initConfig(); diff --git a/src/js/how2validate/utility/log_utility.ts b/src/js/how2validate/utility/log_utility.ts new file mode 100644 index 0000000..9c2f56f --- /dev/null +++ b/src/js/how2validate/utility/log_utility.ts @@ -0,0 +1,29 @@ +import * as logging from 'loglevel'; +import { getActiveSecretStatus, getInactiveSecretStatus } from './config_utility'; + +export function setupLogging() { + logging.setLevel('info'); // Set logging level to INFO +} + +export function getSecretStatusMessage(service: string, isActive: string, response?: boolean, responseData?: any): string { + // Normalize isActive values to handle both 'Active' and 'InActive' + let status: string; + + if (isActive === getActiveSecretStatus()) { + status = "active and operational"; + } else if (isActive === getInactiveSecretStatus()) { + status = "inactive and not operational"; + } else { + throw new Error(`Unexpected isActive value: ${isActive}. Expected 'Active' or 'InActive'.`); + } + + // Base message about the secret's status + let message = `The provided secret '${service}' is currently ${status}.`; + + // If a response exists, append it to the message + if (response) { + message += ` Here is the additional response data:\n${responseData}`; + } + + return message; +} diff --git a/src/js/how2validate/utility/tool_utility.ts b/src/js/how2validate/utility/tool_utility.ts new file mode 100644 index 0000000..d0cdf6e --- /dev/null +++ b/src/js/how2validate/utility/tool_utility.ts @@ -0,0 +1,165 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as logging from 'loglevel'; +import { execSync } from 'child_process'; + +import { getPackageName } from './config_utility'; +import { setupLogging } from './log_utility'; + +// Call the logging setup function +setupLogging(); + +// Get the directory of the current file +const currentDir = __dirname; + +// Path to the TokenManager JSON file +const tokenManager_filePath = path.join(currentDir, '..', '..', 'tokenManager.json'); + +export function getSecretProviders(filePath: string = tokenManager_filePath): string[] { + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + const enabledSecretsServices: string[] = []; + + for (const provider in data) { + const tokens = data[provider]; + for (const tokenInfo of tokens) { + if (tokenInfo.is_enabled) { + enabledSecretsServices.push(provider); + } + } + } + return enabledSecretsServices; +} + +export function getSecretServices(filePath: string = tokenManager_filePath): string[] { + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + const enabledSecretsServices: string[] = []; + + for (const provider in data) { + const tokens = data[provider]; + for (const tokenInfo of tokens) { + if (tokenInfo.is_enabled) { + enabledSecretsServices.push(tokenInfo.display_name); + } + } + } + + return enabledSecretsServices; +} + +export function formatServiceProviders(filePath: string = tokenManager_filePath): string { + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + const enabledSecretsServices: string[] = []; + + for (const provider in data) { + const tokens = data[provider]; + for (const tokenInfo of tokens) { + if (tokenInfo.is_enabled) { + enabledSecretsServices.push(provider); + } + } + } + + return enabledSecretsServices.map(service => ` - ${service}`).join('\n'); +} + +export function formatServices(filePath: string = tokenManager_filePath): string { + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + const enabledSecretsServices: string[] = []; + + for (const provider in data) { + const tokens = data[provider]; + for (const tokenInfo of tokens) { + if (tokenInfo.is_enabled) { + enabledSecretsServices.push(`${provider} - ${tokenInfo.display_name}`); + } + } + } + + return enabledSecretsServices.map(service => ` - ${service}`).join('\n'); +} + +export function formatString(inputString: string): string { + if (typeof inputString !== 'string') { + throw new Error("Input must be a string"); + } + + return inputString.toLowerCase().replace(/ /g, '_'); +} + +export function validateChoice(value: string, validChoices: string[]): string { + const formattedValue = formatString(value); + const formattedChoices = validChoices.map(choice => formatString(choice)); + + if (!formattedChoices.includes(formattedValue)) { + throw new Error(`Invalid choice: '${value}'. Choose from ${validChoices.join(', ')}.`); + } + + return formattedValue; +} + +export function redactSecret(secret: string): string { + if (typeof secret !== 'string') { + throw new Error("Input must be a string"); + } + + if (secret.length <= 5) { + return secret; // Return the secret as is if it's 5 characters or less + } + + return secret.slice(0, 5) + '*'.repeat(secret.length - 5); +} + +export function updateTool(): void { + logging.info("Updating the tool..."); + + // Determine the package manager + const packageManager = detectPackageManager(); + const packageName = getPackageName(); + + try { + switch (packageManager) { + case 'npm': + execSync(`npm install --global ${packageName}`, { stdio: 'inherit' }); + break; + case 'yarn': + execSync(`yarn global add ${packageName}`, { stdio: 'inherit' }); + break; + case 'pnpm': + execSync(`pnpm add --global ${packageName}`, { stdio: 'inherit' }); + break; + case 'bun': + execSync(`bun add ${packageName} --global`, { stdio: 'inherit' }); + break; + default: + console.error("Unsupported package manager. Please install the tool manually."); + return; + } + console.log("Tool updated to the latest version."); + } catch (error) { + console.error(`Failed to update the tool: ${error}`); + } +} + +function detectPackageManager(): string { + try { + execSync('npm --version', { stdio: 'ignore' }); + return 'npm'; + } catch { + try { + execSync('yarn --version', { stdio: 'ignore' }); + return 'yarn'; + } catch { + try { + execSync('pnpm --version', { stdio: 'ignore' }); + return 'pnpm'; + } catch { + try { + execSync('bun --version', { stdio: 'ignore' }); + return 'bun'; + } catch { + return 'unknown'; // For unrecognized package managers + } + } + } + } +} \ No newline at end of file diff --git a/src/js/how2validate/validators/npm/npm_access_token.ts b/src/js/how2validate/validators/npm/npm_access_token.ts new file mode 100644 index 0000000..a2ddbcf --- /dev/null +++ b/src/js/how2validate/validators/npm/npm_access_token.ts @@ -0,0 +1,57 @@ +import axios, { AxiosError } from 'axios'; +import { getActiveSecretStatus, getInactiveSecretStatus } from '../../utility/config_utility'; +import { getSecretStatusMessage } from '../../utility/log_utility'; + +export async function validateNpmAccessToken( + service: string, + secret: string, + response: boolean, + report?: boolean +): Promise { + const url = "https://registry.npmjs.org/-/npm/v1/user"; + const nocacheHeaders = { 'Cache-Control': 'no-cache' }; + const headers = { 'Authorization': `Bearer ${secret}` }; + + try { + const responseData = await axios.get(url, { + headers: { ...nocacheHeaders, ...headers } + }); + + if (responseData.status === 200) { + if (!response) { + return getSecretStatusMessage(service, getActiveSecretStatus() as string, response); + } else { + return getSecretStatusMessage(service, getActiveSecretStatus() as string, response, JSON.stringify(responseData.data, null, 4)); + } + } else { + return handleInactiveStatus(service, response, responseData.data); + } + } catch (error) { + return handleErrors(error, service, response); + } +} + +function handleInactiveStatus(service: string, response: boolean, data?: any): string { + if (!response) { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + } else { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, data ? JSON.stringify(data) : "No additional data."); + } +} + +function handleErrors(error: unknown, service: string, response: boolean): string { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + if (!response) { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + } else { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, axiosError.response?.data || axiosError.message); + } + } + + if (!response) { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + } else { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, "An unexpected error occurred."); + } +} diff --git a/src/js/how2validate/validators/snyk/snyk_auth_key.ts b/src/js/how2validate/validators/snyk/snyk_auth_key.ts new file mode 100644 index 0000000..b3f7af3 --- /dev/null +++ b/src/js/how2validate/validators/snyk/snyk_auth_key.ts @@ -0,0 +1,63 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import axios from 'axios'; +import { getActiveSecretStatus, getInactiveSecretStatus } from '../../utility/config_utility'; +import { getSecretStatusMessage } from '../../utility/log_utility'; + +export async function validateSnykAuthKey( + service: string, + secret: string, + response: boolean, + report?: boolean +): Promise { + // Snyk API endpoint for getting user information + const url = 'https://snyk.io/api/v1/user'; + + // Headers to ensure no caching and to authorize the request using the provided API key (token) + const headers = { + 'Cache-Control': 'no-cache', + Authorization: `token ${secret}`, + }; + + try { + // Send a GET request to the Snyk API + const responseData = await axios.get(url, { headers }); + + // If the request was successful (HTTP 200) + if (responseData.status === 200) { + // If `response` is false, return the active status message without response data + if (!response) { + return getSecretStatusMessage(service, getActiveSecretStatus() as string, response); + } else { + // Return the status message along with the response data + return getSecretStatusMessage(service, getActiveSecretStatus() as string, response, JSON.stringify(responseData.data, null, 4)); + } + } else { + // If the response status code is not 200, treat the secret as inactive + return handleInactiveResponse(service, response, responseData.data); + } + } catch (error) { + // Handle errors and return the inactive status message + return handleError(service, response, error); + } +} + +function handleInactiveResponse(service: string, response: boolean, responseData: any): string { + if (!response) { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + } else { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, responseData); + } +} + +function handleError(service: string, response: boolean, error: any): string { + let errorMessage = ''; + + if (axios.isAxiosError(error)) { + errorMessage = error.response ? error.response.data : error.message; + } else { + errorMessage = error.message; + } + + return handleInactiveResponse(service, response, errorMessage); +} diff --git a/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts b/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts new file mode 100644 index 0000000..bc00eda --- /dev/null +++ b/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts @@ -0,0 +1,58 @@ +import axios, { AxiosError } from 'axios'; + +import { getActiveSecretStatus, getInactiveSecretStatus } from '../../utility/config_utility'; +import { getSecretStatusMessage } from '../../utility/log_utility'; + +export async function validateSonarcloudToken( + service: string, + secret: string, + response: boolean, + report?: boolean +): Promise { + const url = "https://sonarcloud.io/api/users/current"; + const nocacheHeaders = { 'Cache-Control': 'no-cache' }; + const headers = { 'Authorization': `Bearer ${secret}` }; + + try { + const responseData = await axios.get(url, { + headers: { ...nocacheHeaders, ...headers } + }); + + if (responseData.status === 200) { + if (!response) { + return getSecretStatusMessage(service, getActiveSecretStatus() as string, response); + } else { + return getSecretStatusMessage(service, getActiveSecretStatus() as string, response, JSON.stringify(responseData.data, null, 4)); + } + } else { + return handleInactiveStatus(service, response, responseData.data); + } + } catch (error) { + return handleErrors(error, service, response); + } +} + +function handleInactiveStatus(service: string, response: boolean, data?: any): string { + if (!response) { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + } else { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, data ? JSON.stringify(data) : "No additional data."); + } +} + +function handleErrors(error: unknown, service: string, response: boolean): string { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + if (!response) { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + } else { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, axiosError.response?.data || axiosError.message); + } + } + + if (!response) { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + } else { + return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, "An unexpected error occurred."); + } +} diff --git a/src/js/index.ts b/src/js/index.ts deleted file mode 100644 index 6429788..0000000 --- a/src/js/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function hello(){ - console.log("Hello User!") -} \ No newline at end of file diff --git a/src/js/jsr.json b/src/js/jsr.json index cfa0b74..ca8b3cf 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -2,5 +2,15 @@ "name": "@how2validate/how2validate", "version": "0.0.1", "license": "MIT", - "exports": "./index.ts" - } \ No newline at end of file + "exports": "./how2validate/index.ts", + "publish": { + "include": [ + "LICENSE", + "README.md", + "config.ini", + "tokenManager.json", + "how2validate" + ], + "exclude": ["tests"] + } +} \ No newline at end of file diff --git a/src/js/package.json b/src/js/package.json index 48aafdf..3dc936b 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,19 +1,21 @@ { "name": "how2validate", "version": "0.0.1", - "description": "A cli and package for validating secrets.", - "main": "index.ts", + "description": "A CLI tool to validate secrets for different services.", + "main": "how2validate/index.ts", "scripts": { - "test": "node test" + "build": "tsc", + "start": "node dist/index.js" }, "repository": { "type": "git", "url": "git+https://github.com/Blackplums/how2validate.git" }, "keywords": [ - "secret", + "secrets", "how2validate", - "secretvalidate" + "secret-validation", + "secrets-management" ], "author": "Vigneshkna", "license": "MIT", @@ -22,6 +24,15 @@ }, "homepage": "https://github.com/Blackplums/how2validate#readme", "dependencies": { - "jsr": "^0.13.2" + "axios": "^1.7.7", + "commander": "^12.1.0", + "ini": "^5.0.0", + "jsr": "^0.13.2", + "loglevel": "^1.9.2" + }, + "devDependencies": { + "@types/ini": "^4.1.1", + "@types/node": "^22.5.5", + "typescript": "^5.6.2" } } diff --git a/src/js/tokenManager.json b/src/js/tokenManager.json new file mode 100644 index 0000000..1bcd033 --- /dev/null +++ b/src/js/tokenManager.json @@ -0,0 +1,1799 @@ +{ + "Adafruit": [ + { + "secret_type": "adafruit_io_key", + "display_name": "Adafruit Io Key", + "is_enabled": false + } + ], + "Adobe": [ + { + "secret_type": "adobe_client_secret", + "display_name": "Adobe Client Secret", + "is_enabled": false + }, + { + "secret_type": "adobe_device_token", + "display_name": "Adobe Device Token", + "is_enabled": false + }, + { + "secret_type": "adobe_pac_token", + "display_name": "Adobe Pac Token", + "is_enabled": false + }, + { + "secret_type": "adobe_refresh_token", + "display_name": "Adobe Refresh Token", + "is_enabled": false + }, + { + "secret_type": "adobe_service_token", + "display_name": "Adobe Service Token", + "is_enabled": false + }, + { + "secret_type": "adobe_short_lived_access_token", + "display_name": "Adobe Short Lived Access Token", + "is_enabled": false + } + ], + "Aiven": [ + { + "secret_type": "aiven_auth_token", + "display_name": "Aiven Auth Token", + "is_enabled": false + }, + { + "secret_type": "aiven_service_password", + "display_name": "Aiven Service Password", + "is_enabled": false + } + ], + "Alibaba": [ + { + "secret_type": "alibaba_cloud_access_key", + "display_name": "Alibaba Cloud Access Key", + "is_enabled": false + } + ], + "Amazon": [ + { + "secret_type": "amazon_oauth_client", + "display_name": "Amazon Oauth Client", + "is_enabled": false + } + ], + "Amazon AWS": [ + { + "secret_type": "aws_access_key_id", + "display_name": "Aws Access Key Id", + "is_enabled": false + }, + { + "secret_type": "aws_secret_access_key", + "display_name": "Aws Secret Access Key", + "is_enabled": false + }, + { + "secret_type": "aws_session_token", + "display_name": "Aws Session Token", + "is_enabled": false + }, + { + "secret_type": "aws_temporary_access_key_id", + "display_name": "Aws Temporary Access Key Id", + "is_enabled": false + } + ], + "Anthropic": [ + { + "secret_type": "anthropic_api_key", + "display_name": "Anthropic Api Key", + "is_enabled": false + }, + { + "secret_type": "anthropic_session_id", + "display_name": "Anthropic Session Id", + "is_enabled": false + } + ], + "Asana": [ + { + "secret_type": "asana_legacy_format_personal_access_token", + "display_name": "Asana Legacy Format Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "asana_personal_access_token", + "display_name": "Asana Personal Access Token", + "is_enabled": false + } + ], + "Atlassian": [ + { + "secret_type": "atlassian_api_token", + "display_name": "Atlassian Api Token", + "is_enabled": false + }, + { + "secret_type": "atlassian_jwt", + "display_name": "Atlassian Jwt", + "is_enabled": false + } + ], + "Authress": [ + { + "secret_type": "authress_service_client_access_key", + "display_name": "Authress Service Client Access Key", + "is_enabled": false + } + ], + "Azure": [ + { + "secret_type": "azure_active_directory_application_secret", + "display_name": "Azure Active Directory Application Secret", + "is_enabled": false + }, + { + "secret_type": "azure_active_directory_user_credential", + "display_name": "Azure Active Directory User Credential", + "is_enabled": false + }, + { + "secret_type": "azure_apim_direct_management_key", + "display_name": "Azure Apim Direct Management Key", + "is_enabled": false + }, + { + "secret_type": "azure_apim_gateway_key", + "display_name": "Azure Apim Gateway Key", + "is_enabled": false + }, + { + "secret_type": "azure_apim_repository_key", + "display_name": "Azure Apim Repository Key", + "is_enabled": false + }, + { + "secret_type": "azure_apim_subscription_key", + "display_name": "Azure Apim Subscription Key", + "is_enabled": false + }, + { + "secret_type": "azure_app_configuration_connection_string", + "display_name": "Azure App Configuration Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_batch_key_identifiable", + "display_name": "Azure Batch Key Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_cache_for_redis_access_key", + "display_name": "Azure Cache For Redis Access Key", + "is_enabled": false + }, + { + "secret_type": "azure_common_annotated_security_key", + "display_name": "Azure Common Annotated Security Key", + "is_enabled": false + }, + { + "secret_type": "azure_communication_services_connection_string", + "display_name": "Azure Communication Services Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_container_registry_key_identifiable", + "display_name": "Azure Container Registry Key Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_cosmosdb_key_identifiable", + "display_name": "Azure Cosmosdb Key Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_devops_personal_access_token", + "display_name": "Azure Devops Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "azure_event_hub_key_identifiable", + "display_name": "Azure Event Hub Key Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_function_key", + "display_name": "Azure Function Key", + "is_enabled": false + }, + { + "secret_type": "azure_iot_device_connection_string", + "display_name": "Azure Iot Device Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_iot_device_key", + "display_name": "Azure Iot Device Key", + "is_enabled": false + }, + { + "secret_type": "azure_iot_device_provisioning_key", + "display_name": "Azure Iot Device Provisioning Key", + "is_enabled": false + }, + { + "secret_type": "azure_iot_hub_connection_string", + "display_name": "Azure Iot Hub Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_iot_hub_key", + "display_name": "Azure Iot Hub Key", + "is_enabled": false + }, + { + "secret_type": "azure_iot_provisioning_connection_string", + "display_name": "Azure Iot Provisioning Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_management_certificate", + "display_name": "Azure Management Certificate", + "is_enabled": false + }, + { + "secret_type": "azure_ml_web_service_classic_identifiable_key", + "display_name": "Azure Ml Web Service Classic Identifiable Key", + "is_enabled": false + }, + { + "secret_type": "azure_relay_key_identifiable", + "display_name": "Azure Relay Key Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_sas_token", + "display_name": "Azure Sas Token", + "is_enabled": false + }, + { + "secret_type": "azure_search_admin_key", + "display_name": "Azure Search Admin Key", + "is_enabled": false + }, + { + "secret_type": "azure_search_query_key", + "display_name": "Azure Search Query Key", + "is_enabled": false + }, + { + "secret_type": "azure_service_bus_identifiable", + "display_name": "Azure Service Bus Identifiable", + "is_enabled": false + }, + { + "secret_type": "azure_signalr_connection_string", + "display_name": "Azure Signalr Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_sql_connection_string", + "display_name": "Azure Sql Connection String", + "is_enabled": false + }, + { + "secret_type": "azure_sql_password", + "display_name": "Azure Sql Password", + "is_enabled": false + }, + { + "secret_type": "azure_storage_account_key", + "display_name": "Azure Storage Account Key", + "is_enabled": false + }, + { + "secret_type": "azure_web_pub_sub_connection_string", + "display_name": "Azure Web Pub Sub Connection String", + "is_enabled": false + }, + { + "secret_type": "microsoft_corporate_network_user_credential", + "display_name": "Microsoft Corporate Network User Credential", + "is_enabled": false + } + ], + "Baidu": [ + { + "secret_type": "baiducloud_api_accesskey", + "display_name": "Baiducloud Api Accesskey", + "is_enabled": false + } + ], + "Beamer": [ + { + "secret_type": "beamer_api_key", + "display_name": "Beamer Api Key", + "is_enabled": false + } + ], + "Bitbucket": [ + { + "secret_type": "bitbucket_server_personal_access_token", + "display_name": "Bitbucket Server Personal Access Token", + "is_enabled": false + } + ], + "Canadian Digital Service": [ + { + "secret_type": "cds_canada_notify_api_key", + "display_name": "Cds Canada Notify Api Key", + "is_enabled": false + } + ], + "Canva": [ + { + "secret_type": "canva_app_secret", + "display_name": "Canva App Secret", + "is_enabled": false + }, + { + "secret_type": "canva_connect_api_secret", + "display_name": "Canva Connect Api Secret", + "is_enabled": false + }, + { + "secret_type": "canva_secret", + "display_name": "Canva Secret", + "is_enabled": false + } + ], + "Cashfree": [ + { + "secret_type": "cashfree_api_key", + "display_name": "Cashfree Api Key", + "is_enabled": false + } + ], + "Checkout.com": [ + { + "secret_type": "checkout_production_secret_key", + "display_name": "Checkout Production Secret Key", + "is_enabled": false + }, + { + "secret_type": "checkout_test_secret_key", + "display_name": "Checkout Test Secret Key", + "is_enabled": false + } + ], + "Chief Tools": [ + { + "secret_type": "chief_tools_token", + "display_name": "Chief Tools Token", + "is_enabled": false + } + ], + "CircleCI": [ + { + "secret_type": "circleci_bot_access_token", + "display_name": "Circleci Bot Access Token", + "is_enabled": false + }, + { + "secret_type": "circleci_personal_access_token", + "display_name": "Circleci Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "circleci_project_access_token", + "display_name": "Circleci Project Access Token", + "is_enabled": false + }, + { + "secret_type": "circleci_release_integration_token", + "display_name": "Circleci Release Integration Token", + "is_enabled": false + } + ], + "Clojars": [ + { + "secret_type": "clojars_deploy_token", + "display_name": "Clojars Deploy Token", + "is_enabled": false + } + ], + "CloudBees": [ + { + "secret_type": "codeship_credential", + "display_name": "Codeship Credential", + "is_enabled": false + } + ], + "Contentful": [ + { + "secret_type": "contentful_personal_access_token", + "display_name": "Contentful Personal Access Token", + "is_enabled": false + } + ], + "Contributed Systems": [ + { + "secret_type": "contributed_systems_credentials", + "display_name": "Contributed Systems Credentials", + "is_enabled": false + } + ], + "Coveo": [ + { + "secret_type": "coveoaccesstoken", + "display_name": "Coveoaccesstoken", + "is_enabled": false + }, + { + "secret_type": "coveoapikey", + "display_name": "Coveoapikey", + "is_enabled": false + } + ], + "crates.io": [ + { + "secret_type": "cratesio_api_token", + "display_name": "Cratesio Api Token", + "is_enabled": false + } + ], + "Databricks": [ + { + "secret_type": "databricks_access_token", + "display_name": "Databricks Access Token", + "is_enabled": false + } + ], + "Datadog": [ + { + "secret_type": "datadog_api_key", + "display_name": "Datadog Api Key", + "is_enabled": false + }, + { + "secret_type": "datadog_app_key", + "display_name": "Datadog App Key", + "is_enabled": false + } + ], + "Defined Networking": [ + { + "secret_type": "defined_networking_nebula_api_key", + "display_name": "Defined Networking Nebula Api Key", + "is_enabled": false + } + ], + "DevCycle": [ + { + "secret_type": "devcycle_client_api_key", + "display_name": "Devcycle Client Api Key", + "is_enabled": false + }, + { + "secret_type": "devcycle_mobile_api_key", + "display_name": "Devcycle Mobile Api Key", + "is_enabled": false + }, + { + "secret_type": "devcycle_server_api_key", + "display_name": "Devcycle Server Api Key", + "is_enabled": false + } + ], + "DigitalOcean": [ + { + "secret_type": "digitalocean_oauth_token", + "display_name": "Digitalocean Oauth Token", + "is_enabled": false + }, + { + "secret_type": "digitalocean_personal_access_token", + "display_name": "Digitalocean Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "digitalocean_refresh_token", + "display_name": "Digitalocean Refresh Token", + "is_enabled": false + }, + { + "secret_type": "digitalocean_system_token", + "display_name": "Digitalocean System Token", + "is_enabled": false + } + ], + "Discord": [ + { + "secret_type": "discord_bot_token", + "display_name": "Discord Bot Token", + "is_enabled": false + } + ], + "Docker": [ + { + "secret_type": "docker_personal_access_token", + "display_name": "Docker Personal Access Token", + "is_enabled": false + } + ], + "Doppler": [ + { + "secret_type": "doppler_audit_token", + "display_name": "Doppler Audit Token", + "is_enabled": false + }, + { + "secret_type": "doppler_cli_token", + "display_name": "Doppler Cli Token", + "is_enabled": false + }, + { + "secret_type": "doppler_personal_token", + "display_name": "Doppler Personal Token", + "is_enabled": false + }, + { + "secret_type": "doppler_scim_token", + "display_name": "Doppler Scim Token", + "is_enabled": false + }, + { + "secret_type": "doppler_service_account_token", + "display_name": "Doppler Service Account Token", + "is_enabled": false + }, + { + "secret_type": "doppler_service_token", + "display_name": "Doppler Service Token", + "is_enabled": false + } + ], + "Dropbox": [ + { + "secret_type": "dropbox_access_token", + "display_name": "Dropbox Access Token", + "is_enabled": false + }, + { + "secret_type": "dropbox_short_lived_access_token", + "display_name": "Dropbox Short Lived Access Token", + "is_enabled": false + } + ], + "Duffel": [ + { + "secret_type": "duffel_live_access_token", + "display_name": "Duffel Live Access Token", + "is_enabled": false + }, + { + "secret_type": "duffel_test_access_token", + "display_name": "Duffel Test Access Token", + "is_enabled": false + } + ], + "Dynatrace": [ + { + "secret_type": "dynatrace_api_token", + "display_name": "Dynatrace Api Token", + "is_enabled": false + }, + { + "secret_type": "dynatrace_internal_token", + "display_name": "Dynatrace Internal Token", + "is_enabled": false + } + ], + "EasyPost": [ + { + "secret_type": "easypost_production_api_key", + "display_name": "Easypost Production Api Key", + "is_enabled": false + }, + { + "secret_type": "easypost_test_api_key", + "display_name": "Easypost Test Api Key", + "is_enabled": false + } + ], + "eBay": [ + { + "secret_type": "ebay_production_client", + "display_name": "Ebay Production Client", + "is_enabled": false + }, + { + "secret_type": "ebay_sandbox_client", + "display_name": "Ebay Sandbox Client", + "is_enabled": false + } + ], + "Facebook": [ + { + "secret_type": "facebook_access_token", + "display_name": "Facebook Access Token", + "is_enabled": false + } + ], + "Fastly": [ + { + "secret_type": "fastly_api_token", + "display_name": "Fastly Api Token", + "is_enabled": false + } + ], + "Figma": [ + { + "secret_type": "figma_pat", + "display_name": "Figma Pat", + "is_enabled": false + } + ], + "Finicity": [ + { + "secret_type": "finicity_app_key", + "display_name": "Finicity App Key", + "is_enabled": false + } + ], + "Firebase": [ + { + "secret_type": "firebase_cloud_messaging_server_key", + "display_name": "Firebase Cloud Messaging Server Key", + "is_enabled": false + } + ], + "Flickr": [ + { + "secret_type": "flickr_api_key", + "display_name": "Flickr Api Key", + "is_enabled": false + } + ], + "Flutterwave": [ + { + "secret_type": "flutterwave_live_api_secret_key", + "display_name": "Flutterwave Live Api Secret Key", + "is_enabled": false + }, + { + "secret_type": "flutterwave_test_api_secret_key", + "display_name": "Flutterwave Test Api Secret Key", + "is_enabled": false + } + ], + "Frame.io": [ + { + "secret_type": "frameio_developer_token", + "display_name": "Frameio Developer Token", + "is_enabled": false + }, + { + "secret_type": "frameio_jwt", + "display_name": "Frameio Jwt", + "is_enabled": false + } + ], + "FullStory": [ + { + "secret_type": "fullstory_api_key", + "display_name": "Fullstory Api Key", + "is_enabled": false + } + ], + "GitHub": [ + { + "secret_type": "github_app_installation_access_token", + "display_name": "Github App Installation Access Token", + "is_enabled": false + }, + { + "secret_type": "github_oauth_access_token", + "display_name": "Github Oauth Access Token", + "is_enabled": false + }, + { + "secret_type": "github_personal_access_token", + "display_name": "Github Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "github_refresh_token", + "display_name": "Github Refresh Token", + "is_enabled": false + }, + { + "secret_type": "github_ssh_private_key", + "display_name": "Github Ssh Private Key", + "is_enabled": false + }, + { + "secret_type": "github_test_token", + "display_name": "Github Test Token", + "is_enabled": false + } + ], + "GitHub Secret Scanning": [ + { + "secret_type": "secret_scanning_sample_token", + "display_name": "Secret Scanning Sample Token", + "is_enabled": false + } + ], + "GitLab": [ + { + "secret_type": "gitlab_access_token", + "display_name": "Gitlab Access Token", + "is_enabled": false + } + ], + "GoCardless": [ + { + "secret_type": "gocardless_live_access_token", + "display_name": "Gocardless Live Access Token", + "is_enabled": false + }, + { + "secret_type": "gocardless_sandbox_access_token", + "display_name": "Gocardless Sandbox Access Token", + "is_enabled": false + } + ], + "Google": [ + { + "secret_type": "google_api_key", + "display_name": "Google Api Key", + "is_enabled": false + }, + { + "secret_type": "google_cloud_private_key_id", + "display_name": "Google Cloud Private Key Id", + "is_enabled": false + }, + { + "secret_type": "google_cloud_service_account_credentials", + "display_name": "Google Cloud Service Account Credentials", + "is_enabled": false + }, + { + "secret_type": "google_cloud_storage_access_key_secret", + "display_name": "Google Cloud Storage Access Key Secret", + "is_enabled": false + }, + { + "secret_type": "google_cloud_storage_service_account_access_key_id", + "display_name": "Google Cloud Storage Service Account Access Key Id", + "is_enabled": false + }, + { + "secret_type": "google_cloud_storage_access_key_secret", + "display_name": "Google Cloud Storage Access Key Secret", + "is_enabled": false + }, + { + "secret_type": "google_cloud_storage_user_access_key_id", + "display_name": "Google Cloud Storage User Access Key Id", + "is_enabled": false + }, + { + "secret_type": "google_oauth_access_token", + "display_name": "Google Oauth Access Token", + "is_enabled": false + }, + { + "secret_type": "google_oauth_client_id", + "display_name": "Google Oauth Client Id", + "is_enabled": false + }, + { + "secret_type": "google_oauth_client_secret", + "display_name": "Google Oauth Client Secret", + "is_enabled": false + }, + { + "secret_type": "google_oauth_refresh_token", + "display_name": "Google Oauth Refresh Token", + "is_enabled": false + } + ], + "Grafana": [ + { + "secret_type": "grafana_cloud_api_key", + "display_name": "Grafana Cloud Api Key", + "is_enabled": false + }, + { + "secret_type": "grafana_cloud_api_token", + "display_name": "Grafana Cloud Api Token", + "is_enabled": false + }, + { + "secret_type": "grafana_project_api_key", + "display_name": "Grafana Project Api Key", + "is_enabled": false + }, + { + "secret_type": "grafana_project_service_account_token", + "display_name": "Grafana Project Service Account Token", + "is_enabled": false + } + ], + "HashiCorp": [ + { + "secret_type": "hashicorp_vault_batch_token", + "display_name": "Hashicorp Vault Batch Token", + "is_enabled": false + }, + { + "secret_type": "hashicorp_vault_root_service_token", + "display_name": "Hashicorp Vault Root Service Token", + "is_enabled": false + }, + { + "secret_type": "hashicorp_vault_service_token", + "display_name": "Hashicorp Vault Service Token", + "is_enabled": false + }, + { + "secret_type": "terraform_api_token", + "display_name": "Terraform Api Token", + "is_enabled": false + } + ], + "Highnote": [ + { + "secret_type": "highnote_rk_live_key", + "display_name": "Highnote Rk Live Key", + "is_enabled": false + }, + { + "secret_type": "highnote_rk_test_key", + "display_name": "Highnote Rk Test Key", + "is_enabled": false + }, + { + "secret_type": "highnote_sk_live_key", + "display_name": "Highnote Sk Live Key", + "is_enabled": false + }, + { + "secret_type": "highnote_sk_test_key", + "display_name": "Highnote Sk Test Key", + "is_enabled": false + } + ], + "HOP": [ + { + "secret_type": "hop_bearer", + "display_name": "Hop Bearer", + "is_enabled": false + }, + { + "secret_type": "hop_pat", + "display_name": "Hop Pat", + "is_enabled": false + }, + { + "secret_type": "hop_ptk", + "display_name": "Hop Ptk", + "is_enabled": false + } + ], + "Hubspot": [ + { + "secret_type": "hubspot_api_key", + "display_name": "Hubspot Api Key", + "is_enabled": false + }, + { + "secret_type": "hubspot_personal_access_key", + "display_name": "Hubspot Personal Access Key", + "is_enabled": false + }, + { + "secret_type": "hubspot_smtp_credential", + "display_name": "Hubspot Smtp Credential", + "is_enabled": false + } + ], + "Hugging Face": [ + { + "secret_type": "hf_org_api_key", + "display_name": "Hf Org Api Key", + "is_enabled": false + }, + { + "secret_type": "hf_user_access_token", + "display_name": "Hf User Access Token", + "is_enabled": false + } + ], + "IBM": [ + { + "secret_type": "ibm_cloud_iam_key", + "display_name": "Ibm Cloud Iam Key", + "is_enabled": false + }, + { + "secret_type": "ibm_softlayer_api_key", + "display_name": "Ibm Softlayer Api Key", + "is_enabled": false + } + ], + "Intercom": [ + { + "secret_type": "intercom_access_token", + "display_name": "Intercom Access Token", + "is_enabled": false + } + ], + "Ionic": [ + { + "secret_type": "ionic_personal_access_token", + "display_name": "Ionic Personal Access Token", + "is_enabled": false + }, + { + "secret_type": "ionic_refresh_token", + "display_name": "Ionic Refresh Token", + "is_enabled": false + } + ], + "JFrog": [ + { + "secret_type": "jfrog_platform_access_token", + "display_name": "Jfrog Platform Access Token", + "is_enabled": false + }, + { + "secret_type": "jfrog_platform_api_key", + "display_name": "Jfrog Platform Api Key", + "is_enabled": false + }, + { + "secret_type": "jfrog_platform_reference_token", + "display_name": "Jfrog Platform Reference Token", + "is_enabled": false + } + ], + "LaunchDarkly": [ + { + "secret_type": "launchdarkly_access_token", + "display_name": "Launchdarkly Access Token", + "is_enabled": false + } + ], + "Lightspeed": [ + { + "secret_type": "lightspeed_xs_pat", + "display_name": "Lightspeed Xs Pat", + "is_enabled": false + } + ], + "Linear": [ + { + "secret_type": "linear_api_key", + "display_name": "Linear Api Key", + "is_enabled": false + }, + { + "secret_type": "linear_oauth_access_token", + "display_name": "Linear Oauth Access Token", + "is_enabled": false + } + ], + "Lob": [ + { + "secret_type": "lob_live_api_key", + "display_name": "Lob Live Api Key", + "is_enabled": false + }, + { + "secret_type": "lob_test_api_key", + "display_name": "Lob Test Api Key", + "is_enabled": false + } + ], + "Localstack": [ + { + "secret_type": "localstack_api_key", + "display_name": "Localstack Api Key", + "is_enabled": false + } + ], + "LogicMonitor": [ + { + "secret_type": "logicmonitor_bearer_token", + "display_name": "Logicmonitor Bearer Token", + "is_enabled": false + }, + { + "secret_type": "logicmonitor_lmv1_access_key", + "display_name": "Logicmonitor Lmv1 Access Key", + "is_enabled": false + } + ], + "Mailchimp": [ + { + "secret_type": "mailchimp_api_key", + "display_name": "Mailchimp Api Key", + "is_enabled": false + }, + { + "secret_type": "mandrill_api_key", + "display_name": "Mandrill Api Key", + "is_enabled": false + } + ], + "Mailgun": [ + { + "secret_type": "mailgun_api_key", + "display_name": "Mailgun Api Key", + "is_enabled": false + }, + { + "secret_type": "mailgun_smtp_credential", + "display_name": "Mailgun Smtp Credential", + "is_enabled": false + } + ], + "Mapbox": [ + { + "secret_type": "mapbox_secret_access_token", + "display_name": "Mapbox Secret Access Token", + "is_enabled": false + } + ], + "MaxMind": [ + { + "secret_type": "maxmind_license_key", + "display_name": "Maxmind License Key", + "is_enabled": false + } + ], + "Mercury": [ + { + "secret_type": "mercury_non_production_api_token", + "display_name": "Mercury Non Production Api Token", + "is_enabled": false + }, + { + "secret_type": "mercury_production_api_token", + "display_name": "Mercury Production Api Token", + "is_enabled": false + } + ], + "Mergify": [ + { + "secret_type": "mergify_application_key", + "display_name": "Mergify Application Key", + "is_enabled": false + } + ], + "MessageBird": [ + { + "secret_type": "messagebird_api_key", + "display_name": "Messagebird Api Key", + "is_enabled": false + } + ], + "Microsoft Teams": [ + { + "secret_type": "microsoft_teams_incoming_webhook_url", + "display_name": "Microsoft Teams Incoming Webhook Url", + "is_enabled": false + } + ], + "Midtrans": [ + { + "secret_type": "midtrans_production_server_key", + "display_name": "Midtrans Production Server Key", + "is_enabled": false + }, + { + "secret_type": "midtrans_sandbox_server_key", + "display_name": "Midtrans Sandbox Server Key", + "is_enabled": false + } + ], + "MongoDB": [ + { + "secret_type": "mongodb_connectionstring", + "display_name": "Mongodb Connectionstring", + "is_enabled": false + } + ], + "New Relic": [ + { + "secret_type": "new_relic_insights_query_key", + "display_name": "New Relic Insights Query Key", + "is_enabled": false + }, + { + "secret_type": "new_relic_license_key", + "display_name": "New Relic License Key", + "is_enabled": false + }, + { + "secret_type": "new_relic_personal_api_key", + "display_name": "New Relic Personal Api Key", + "is_enabled": false + }, + { + "secret_type": "new_relic_rest_api_key", + "display_name": "New Relic Rest Api Key", + "is_enabled": false + } + ], + "Notion": [ + { + "secret_type": "notion_integration_token", + "display_name": "Notion Integration Token", + "is_enabled": false + }, + { + "secret_type": "notion_oauth_client_secret", + "display_name": "Notion Oauth Client Secret", + "is_enabled": false + } + ], + "NPM": [ + { + "secret_type": "npm_access_token", + "display_name": "NPM Access Token", + "is_enabled": true + } + ], + "NuGet": [ + { + "secret_type": "nuget_api_key", + "display_name": "Nuget Api Key", + "is_enabled": false + } + ], + "Octopus Deploy": [ + { + "secret_type": "octopus_deploy_api_key", + "display_name": "Octopus Deploy Api Key", + "is_enabled": false + } + ], + "Oculus": [ + { + "secret_type": "oculus_access_token", + "display_name": "Oculus Access Token", + "is_enabled": false + } + ], + "OneChronos": [ + { + "secret_type": "onechronos_api_key", + "display_name": "Onechronos Api Key", + "is_enabled": false + }, + { + "secret_type": "onechronos_eb_api_key", + "display_name": "Onechronos Eb Api Key", + "is_enabled": false + }, + { + "secret_type": "onechronos_eb_encryption_key", + "display_name": "Onechronos Eb Encryption Key", + "is_enabled": false + }, + { + "secret_type": "onechronos_oauth_token", + "display_name": "Onechronos Oauth Token", + "is_enabled": false + }, + { + "secret_type": "onechronos_refresh_token", + "display_name": "Onechronos Refresh Token", + "is_enabled": false + } + ], + "Onfido": [ + { + "secret_type": "onfido_live_api_token", + "display_name": "Onfido Live Api Token", + "is_enabled": false + }, + { + "secret_type": "onfido_sandbox_api_token", + "display_name": "Onfido Sandbox Api Token", + "is_enabled": false + } + ], + "OpenAI": [ + { + "secret_type": "openai_api_key", + "display_name": "Openai Api Key", + "is_enabled": false + } + ], + "Orbit": [ + { + "secret_type": "orbit_api_token", + "display_name": "Orbit Api Token", + "is_enabled": false + } + ], + "PagerDuty": [ + { + "secret_type": "pagerduty_oauth_secret", + "display_name": "Pagerduty Oauth Secret", + "is_enabled": false + }, + { + "secret_type": "pagerduty_oauth_token", + "display_name": "Pagerduty Oauth Token", + "is_enabled": false + }, + { + "secret_type": "pagerduty_api_key", + "display_name": "Pagerduty Api Key", + "is_enabled": false + } + ], + "Palantir": [ + { + "secret_type": "palantir_jwt", + "display_name": "Palantir Jwt", + "is_enabled": false + } + ], + "Persona Identities": [ + { + "secret_type": "persona_production_api_key", + "display_name": "Persona Production Api Key", + "is_enabled": false + }, + { + "secret_type": "persona_sandbox_api_key", + "display_name": "Persona Sandbox Api Key", + "is_enabled": false + } + ], + "Pinterest": [ + { + "secret_type": "pinterest_access_token", + "display_name": "Pinterest Access Token", + "is_enabled": false + }, + { + "secret_type": "pinterest_refresh_token", + "display_name": "Pinterest Refresh Token", + "is_enabled": false + } + ], + "PlanetScale": [ + { + "secret_type": "planetscale_database_password", + "display_name": "Planetscale Database Password", + "is_enabled": false + }, + { + "secret_type": "planetscale_oauth_token", + "display_name": "Planetscale Oauth Token", + "is_enabled": false + }, + { + "secret_type": "planetscale_service_token", + "display_name": "Planetscale Service Token", + "is_enabled": false + } + ], + "Plivo": [ + { + "secret_type": "plivo_auth_id", + "display_name": "Plivo Auth Id", + "is_enabled": false + }, + { + "secret_type": "plivo_auth_token", + "display_name": "Plivo Auth Token", + "is_enabled": false + } + ], + "Postman": [ + { + "secret_type": "postman_api_key", + "display_name": "Postman Api Key", + "is_enabled": false + }, + { + "secret_type": "postman_collection_key", + "display_name": "Postman Collection Key", + "is_enabled": false + } + ], + "Prefect": [ + { + "secret_type": "prefect_server_api_key", + "display_name": "Prefect Server Api Key", + "is_enabled": false + }, + { + "secret_type": "prefect_user_api_key", + "display_name": "Prefect User Api Key", + "is_enabled": false + } + ], + "Proctorio": [ + { + "secret_type": "proctorio_consumer_key", + "display_name": "Proctorio Consumer Key", + "is_enabled": false + }, + { + "secret_type": "proctorio_linkage_key", + "display_name": "Proctorio Linkage Key", + "is_enabled": false + }, + { + "secret_type": "proctorio_registration_key", + "display_name": "Proctorio Registration Key", + "is_enabled": false + }, + { + "secret_type": "proctorio_secret_key", + "display_name": "Proctorio Secret Key", + "is_enabled": false + } + ], + "Pulumi": [ + { + "secret_type": "pulumi_access_token", + "display_name": "Pulumi Access Token", + "is_enabled": false + } + ], + "PyPI": [ + { + "secret_type": "pypi_api_token", + "display_name": "Pypi Api Token", + "is_enabled": false + } + ], + "ReadMe": [ + { + "secret_type": "readmeio_api_access_token", + "display_name": "Readmeio Api Access Token", + "is_enabled": false + } + ], + "redirect.pizza": [ + { + "secret_type": "redirect_pizza_api_token", + "display_name": "Redirect Pizza Api Token", + "is_enabled": false + } + ], + "Replicate": [ + { + "secret_type": "replicate_api_token", + "display_name": "Replicate Api Token", + "is_enabled": false + } + ], + "Rootly": [ + { + "secret_type": "rootly_api_key", + "display_name": "Rootly Api Key", + "is_enabled": false + } + ], + "RubyGems": [ + { + "secret_type": "rubygems_api_key", + "display_name": "Rubygems Api Key", + "is_enabled": false + } + ], + "Samsara": [ + { + "secret_type": "samsara_api_token", + "display_name": "Samsara Api Token", + "is_enabled": false + }, + { + "secret_type": "samsara_oauth_access_token", + "display_name": "Samsara Oauth Access Token", + "is_enabled": false + } + ], + "Segment": [ + { + "secret_type": "segment_public_api_token", + "display_name": "Segment Public Api Token", + "is_enabled": false + } + ], + "SendGrid": [ + { + "secret_type": "sendgrid_api_key", + "display_name": "Sendgrid Api Key", + "is_enabled": false + } + ], + "Sendinblue": [ + { + "secret_type": "sendinblue_api_key", + "display_name": "Sendinblue Api Key", + "is_enabled": false + }, + { + "secret_type": "sendinblue_smtp_key", + "display_name": "Sendinblue Smtp Key", + "is_enabled": false + } + ], + "Sentry": [ + { + "secret_type": "sentry_auth_token", + "display_name": "Sentry Auth Token", + "is_enabled": false + } + ], + "Shippo": [ + { + "secret_type": "shippo_live_api_token", + "display_name": "Shippo Live Api Token", + "is_enabled": false + }, + { + "secret_type": "shippo_test_api_token", + "display_name": "Shippo Test Api Token", + "is_enabled": false + } + ], + "Shopee": [ + { + "secret_type": "shopee_open_platform_partner_key", + "display_name": "Shopee Open Platform Partner Key", + "is_enabled": false + } + ], + "Shopify": [ + { + "secret_type": "shopify_access_token", + "display_name": "Shopify Access Token", + "is_enabled": false + }, + { + "secret_type": "shopify_app_client_credentials", + "display_name": "Shopify App Client Credentials", + "is_enabled": false + }, + { + "secret_type": "shopify_app_client_secret", + "display_name": "Shopify App Client Secret", + "is_enabled": false + }, + { + "secret_type": "shopify_app_shared_secret", + "display_name": "Shopify App Shared Secret", + "is_enabled": false + }, + { + "secret_type": "shopify_custom_app_access_token", + "display_name": "Shopify Custom App Access Token", + "is_enabled": false + }, + { + "secret_type": "shopify_marketplace_token", + "display_name": "Shopify Marketplace Token", + "is_enabled": false + }, + { + "secret_type": "shopify_merchant_token", + "display_name": "Shopify Merchant Token", + "is_enabled": false + }, + { + "secret_type": "shopify_partner_api_token", + "display_name": "Shopify Partner Api Token", + "is_enabled": false + }, + { + "secret_type": "shopify_private_app_password", + "display_name": "Shopify Private App Password", + "is_enabled": false + } + ], + "Siemens": [ + { + "secret_type": "siemens_api_token", + "display_name": "Siemens Api Token", + "is_enabled": false + } + ], + "Slack": [ + { + "secret_type": "slack_api_token", + "display_name": "Slack Api Token", + "is_enabled": false + }, + { + "secret_type": "slack_incoming_webhook_url", + "display_name": "Slack Incoming Webhook Url", + "is_enabled": false + }, + { + "secret_type": "slack_workflow_webhook_url", + "display_name": "Slack Workflow Webhook Url", + "is_enabled": false + } + ], + "Snyk": [ + { + "secret_type": "snyk_auth_key", + "display_name": "Snyk Auth Key", + "is_enabled": true + } + ], + "SonarCloud": [ + { + "secret_type": "sonarcloud_token", + "display_name": "Sonarcloud Token", + "is_enabled": true + } + ], + "Square": [ + { + "secret_type": "square_access_token", + "display_name": "Square Access Token", + "is_enabled": false + }, + { + "secret_type": "square_production_application_secret", + "display_name": "Square Production Application Secret", + "is_enabled": false + }, + { + "secret_type": "square_sandbox_application_secret", + "display_name": "Square Sandbox Application Secret", + "is_enabled": false + } + ], + "SSLMate": [ + { + "secret_type": "sslmate_api_key", + "display_name": "Sslmate Api Key", + "is_enabled": false + }, + { + "secret_type": "sslmate_cluster_secret", + "display_name": "Sslmate Cluster Secret", + "is_enabled": false + } + ], + "Stripe": [ + { + "secret_type": "stripe_api_key", + "display_name": "Stripe Api Key", + "is_enabled": false + }, + { + "secret_type": "stripe_legacy_api_key", + "display_name": "Stripe Legacy Api Key", + "is_enabled": false + }, + { + "secret_type": "stripe_live_restricted_key", + "display_name": "Stripe Live Restricted Key", + "is_enabled": false + }, + { + "secret_type": "stripe_test_restricted_key", + "display_name": "Stripe Test Restricted Key", + "is_enabled": false + }, + { + "secret_type": "stripe_test_secret_key", + "display_name": "Stripe Test Secret Key", + "is_enabled": false + }, + { + "secret_type": "stripe_webhook_signing_secret", + "display_name": "Stripe Webhook Signing Secret", + "is_enabled": false + } + ], + "Supabase": [ + { + "secret_type": "supabase_service_key", + "display_name": "Supabase Service Key", + "is_enabled": false + } + ], + "Tableau": [ + { + "secret_type": "tableau_personal_access_token", + "display_name": "Tableau Personal Access Token", + "is_enabled": false + } + ], + "Telegram": [ + { + "secret_type": "telegram_bot_token", + "display_name": "Telegram Bot Token", + "is_enabled": false + } + ], + "Telnyx": [ + { + "secret_type": "telnyx_api_v2_key", + "display_name": "Telnyx Api V2 Key", + "is_enabled": false + } + ], + "Tencent": [ + { + "secret_type": "tencent_cloud_secret_id", + "display_name": "Tencent Cloud Secret Id", + "is_enabled": false + }, + { + "secret_type": "tencent_wechat_api_app_id", + "display_name": "Tencent Wechat Api App Id", + "is_enabled": false + } + ], + "Thunderstore": [ + { + "secret_type": "thunderstore_io_api_token", + "display_name": "Thunderstore Io Api Token", + "is_enabled": false + } + ], + "Twilio": [ + { + "secret_type": "twilio_access_token", + "display_name": "Twilio Access Token", + "is_enabled": false + }, + { + "secret_type": "twilio_account_sid", + "display_name": "Twilio Account Sid", + "is_enabled": false + }, + { + "secret_type": "twilio_api_key", + "display_name": "Twilio Api Key", + "is_enabled": false + } + ], + "Typeform": [ + { + "secret_type": "typeform_personal_access_token", + "display_name": "Typeform Personal Access Token", + "is_enabled": false + } + ], + "Uniwise": [ + { + "secret_type": "wiseflow_api_key", + "display_name": "Wiseflow Api Key", + "is_enabled": false + } + ], + "Unkey": [ + { + "secret_type": "unkey_root_key", + "display_name": "Unkey Root Key", + "is_enabled": false + } + ], + "VolcEngine": [ + { + "secret_type": "volcengine_access_key_id", + "display_name": "Volcengine Access Key Id", + "is_enabled": false + } + ], + "Wakatime": [ + { + "secret_type": "wakatime_api_key", + "display_name": "Wakatime Api Key", + "is_enabled": false + }, + { + "secret_type": "wakatime_app_secret", + "display_name": "Wakatime App Secret", + "is_enabled": false + }, + { + "secret_type": "wakatime_oauth_access_token", + "display_name": "Wakatime Oauth Access Token", + "is_enabled": false + }, + { + "secret_type": "wakatime_oauth_refresh_token", + "display_name": "Wakatime Oauth Refresh Token", + "is_enabled": false + } + ], + "Workato": [ + { + "secret_type": "workato_developer_api_token", + "display_name": "Workato Developer Api Token", + "is_enabled": false + } + ], + "WorkOS": [ + { + "secret_type": "workos_production_api_key", + "display_name": "Workos Production Api Key", + "is_enabled": false + }, + { + "secret_type": "workos_staging_api_key", + "display_name": "Workos Staging Api Key", + "is_enabled": false + } + ], + "Yandex": [ + { + "secret_type": "yandex_cloud_api_key", + "display_name": "Yandex Cloud Api Key", + "is_enabled": false + }, + { + "secret_type": "yandex_cloud_iam_access_secret", + "display_name": "Yandex Cloud Iam Access Secret", + "is_enabled": false + }, + { + "secret_type": "yandex_cloud_iam_cookie", + "display_name": "Yandex Cloud Iam Cookie", + "is_enabled": false + }, + { + "secret_type": "yandex_cloud_iam_token", + "display_name": "Yandex Cloud Iam Token", + "is_enabled": false + }, + { + "secret_type": "yandex_cloud_smartcaptcha_server_key", + "display_name": "Yandex Cloud Smartcaptcha Server Key", + "is_enabled": false + }, + { + "secret_type": "yandex_dictionary_api_key", + "display_name": "Yandex Dictionary Api Key", + "is_enabled": false + }, + { + "secret_type": "yandex_passport_oauth_token", + "display_name": "Yandex Passport Oauth Token", + "is_enabled": false + }, + { + "secret_type": "yandex_predictor_api_key", + "display_name": "Yandex Predictor Api Key", + "is_enabled": false + }, + { + "secret_type": "yandex_translate_api_key", + "display_name": "Yandex Translate Api Key", + "is_enabled": false + } + ], + "Zuplo": [ + { + "secret_type": "zuplo_consumer_api_key", + "display_name": "Zuplo Consumer Api Key", + "is_enabled": false + } + ] +} \ No newline at end of file diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json new file mode 100644 index 0000000..061caa1 --- /dev/null +++ b/src/js/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES6", // JavaScript version to compile to + "module": "commonjs", // Module system (Node.js uses CommonJS) + "outDir": "./dist", // Output directory for compiled JS files + "rootDir": "./how2validate", // Directory containing TypeScript source files + "strict": true, // Enable all strict type-checking options + "esModuleInterop": true, // Enable ES6 import/export interoperability + "skipLibCheck": true, // Skip type checking of declaration files + "forceConsistentCasingInFileNames": true // Ensure consistent file name casing + }, + "include": ["how2validate/**/*"] // Specifies which files to include in the project +} From 7db10ffc267acb38ab0e78343f51714738c365d8 Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Mon, 23 Sep 2024 20:54:47 +0530 Subject: [PATCH 007/106] refactor(semver): updated sem ver --- src/js/.releaserc | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/js/package.json | 6 ++++++ 2 files changed, 51 insertions(+) create mode 100644 src/js/.releaserc diff --git a/src/js/.releaserc b/src/js/.releaserc new file mode 100644 index 0000000..dc3dc25 --- /dev/null +++ b/src/js/.releaserc @@ -0,0 +1,45 @@ +{ + "branches": ["main"], + "tagFormat": "${version}", + "repositoryUrl": "https://github.com/Blackplums/how2validate", + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { "breaking": true, "release": "major" }, + { "revert": true, "release": "patch" }, + { "type": "build", "release": "patch" }, + { "type": "docs", "release": "patch" }, + { "type": "feat", "release": "minor" }, + { "type": "fix", "release": "patch" }, + { "type": "perf", "release": "patch" }, + { "type": "refactor", "release": "patch" }, + { "type": "chore", "release": "patch" } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { "type": "build", "section": "Build", "hidden": false }, + { "type": "chore", "section": "Chores", "hidden": false }, + { "type": "ci", "section": "CI/CD", "hidden": false }, + { "type": "docs", "section": "Docs", "hidden": false }, + { "type": "feat", "section": "Features", "hidden": false }, + { "type": "fix", "section": "Bug Fixes", "hidden": false }, + { "type": "perf", "section": "Performance", "hidden": false }, + { "type": "refactor", "section": "Refactor", "hidden": false }, + { "type": "style", "section": "Code Style", "hidden": false }, + { "type": "test", "section": "Tests", "hidden": false } + ] + } + } + ], + "@semantic-release/github" + ] + } \ No newline at end of file diff --git a/src/js/package.json b/src/js/package.json index 3dc936b..493455e 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -31,8 +31,14 @@ "loglevel": "^1.9.2" }, "devDependencies": { + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/commit-analyzer": "^13.0.0", + "@semantic-release/git": "^10.0.1", + "@semantic-release/npm": "^12.0.1", + "@semantic-release/release-notes-generator": "^14.0.1", "@types/ini": "^4.1.1", "@types/node": "^22.5.5", + "semantic-release": "^24.1.1", "typescript": "^5.6.2" } } From 591c263f74ecb60d616a339351ddcfe6a0985edc Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Mon, 23 Sep 2024 20:59:27 +0530 Subject: [PATCH 008/106] fix(version): beta-0 version update --- src/js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/package.json b/src/js/package.json index 493455e..0a4687a 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,6 +1,6 @@ { "name": "how2validate", - "version": "0.0.1", + "version": "0.0.2-beta.0", "description": "A CLI tool to validate secrets for different services.", "main": "how2validate/index.ts", "scripts": { From bc79a4f8065506c6e016d259b3644a5ab9ac0f55 Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Mon, 23 Sep 2024 21:00:30 +0530 Subject: [PATCH 009/106] fix(version):updated beta version publish --- src/js/jsr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/jsr.json b/src/js/jsr.json index ca8b3cf..5a4d911 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.1", + "version": "0.0.2-beta-0", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { From 58bb708e26b8fd4aabc3ad87f725a71269c9571c Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Mon, 23 Sep 2024 23:15:17 +0530 Subject: [PATCH 010/106] fix(config path): updated config files --- src/js/jsr.json | 2 +- src/js/package.json | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/js/jsr.json b/src/js/jsr.json index 5a4d911..d2c5058 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.2-beta-0", + "version": "0.0.2-beta.1", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { diff --git a/src/js/package.json b/src/js/package.json index 0a4687a..3e49e11 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,10 +1,10 @@ { "name": "how2validate", - "version": "0.0.2-beta.0", + "version": "0.0.2-beta.1", "description": "A CLI tool to validate secrets for different services.", "main": "how2validate/index.ts", "scripts": { - "build": "tsc", + "build": "npx tsc && npx cpx './config.ini' dist/ && npx cpx './tokenManager.json' dist/", "start": "node dist/index.js" }, "repository": { @@ -38,7 +38,12 @@ "@semantic-release/release-notes-generator": "^14.0.1", "@types/ini": "^4.1.1", "@types/node": "^22.5.5", + "cpx": "^1.5.0", "semantic-release": "^24.1.1", "typescript": "^5.6.2" - } + }, + "files": [ + "config.ini", + "tokenManager.json" + ] } From b2a937c4410903708e743d16fd0020dcd4d11926 Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Tue, 24 Sep 2024 00:07:54 +0530 Subject: [PATCH 011/106] fix(utility): config utility --- src/js/LICENSE | 21 +++++++++++++++++++ src/js/how2validate/utility/config_utility.ts | 5 +++++ src/js/jsr.json | 2 +- src/js/tsconfig.json | 6 ++++-- 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 src/js/LICENSE diff --git a/src/js/LICENSE b/src/js/LICENSE new file mode 100644 index 0000000..e938edb --- /dev/null +++ b/src/js/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 BlackPlum + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/js/how2validate/utility/config_utility.ts b/src/js/how2validate/utility/config_utility.ts index 2665c64..7c59003 100644 --- a/src/js/how2validate/utility/config_utility.ts +++ b/src/js/how2validate/utility/config_utility.ts @@ -1,6 +1,11 @@ import * as path from 'path'; import * as fs from 'fs'; import * as ini from 'ini'; +import { fileURLToPath } from 'url'; + +// Get the current file's path in ES module format +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); interface Config { DEFAULT?: { diff --git a/src/js/jsr.json b/src/js/jsr.json index d2c5058..b456e2b 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.2-beta.1", + "version": "0.0.2-beta.2", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index 061caa1..7d7975d 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -1,13 +1,15 @@ { "compilerOptions": { "target": "ES6", // JavaScript version to compile to - "module": "commonjs", // Module system (Node.js uses CommonJS) + "module": "ESNext", // Module system (Node.js uses CommonJS) "outDir": "./dist", // Output directory for compiled JS files "rootDir": "./how2validate", // Directory containing TypeScript source files "strict": true, // Enable all strict type-checking options "esModuleInterop": true, // Enable ES6 import/export interoperability "skipLibCheck": true, // Skip type checking of declaration files - "forceConsistentCasingInFileNames": true // Ensure consistent file name casing + "forceConsistentCasingInFileNames": true, // Ensure consistent file name casing + "moduleResolution": "node", // Ensures Node.js-style resolution + "resolveJsonModule": true // Allows importing .json files directly }, "include": ["how2validate/**/*"] // Specifies which files to include in the project } From e456a51d89557d11c7cbc95915224d5ee0d7588e Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Tue, 24 Sep 2024 00:33:56 +0530 Subject: [PATCH 012/106] fix(path): prod utility path --- src/js/how2validate/utility/config_utility.ts | 18 ++++++++++++------ src/js/how2validate/utility/log_utility.ts | 2 +- src/js/jsr.json | 2 +- src/js/package.json | 2 +- src/js/tsconfig.json | 8 +++----- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/js/how2validate/utility/config_utility.ts b/src/js/how2validate/utility/config_utility.ts index 7c59003..98eb7b6 100644 --- a/src/js/how2validate/utility/config_utility.ts +++ b/src/js/how2validate/utility/config_utility.ts @@ -1,11 +1,6 @@ import * as path from 'path'; import * as fs from 'fs'; import * as ini from 'ini'; -import { fileURLToPath } from 'url'; - -// Get the current file's path in ES module format -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); interface Config { DEFAULT?: { @@ -20,9 +15,20 @@ interface Config { let config: Config | null = null; +function getConfigFilePath() { + if (process.env.NODE_ENV === 'production') { + // Assuming your config.ini is in the dist folder when published + return path.resolve(__dirname, '..', 'config.ini'); + } else { + // For local development + return path.resolve(__dirname, '..', '..', 'config.ini'); + } + } + export function initConfig() { // Path to the config.ini file, relative to where your compiled JavaScript will be running (in dist) - const configFilePath = path.resolve(__dirname, '..', '..', 'config.ini'); // Adjusted for dist directory + const configFilePath = getConfigFilePath(); + //const configFilePath = path.resolve(__dirname, '..', '..', 'config.ini'); // Adjusted for dist directory try { const configContent = fs.readFileSync(configFilePath, 'utf-8'); diff --git a/src/js/how2validate/utility/log_utility.ts b/src/js/how2validate/utility/log_utility.ts index 9c2f56f..a27ee9a 100644 --- a/src/js/how2validate/utility/log_utility.ts +++ b/src/js/how2validate/utility/log_utility.ts @@ -2,7 +2,7 @@ import * as logging from 'loglevel'; import { getActiveSecretStatus, getInactiveSecretStatus } from './config_utility'; export function setupLogging() { - logging.setLevel('info'); // Set logging level to INFO + logging.setLevel("INFO"); // Set logging level to INFO } export function getSecretStatusMessage(service: string, isActive: string, response?: boolean, responseData?: any): string { diff --git a/src/js/jsr.json b/src/js/jsr.json index b456e2b..b40592d 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.2-beta.2", + "version": "0.0.2-beta.3", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { diff --git a/src/js/package.json b/src/js/package.json index 3e49e11..fd1a1ae 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,6 +1,6 @@ { "name": "how2validate", - "version": "0.0.2-beta.1", + "version": "0.0.2-beta.3", "description": "A CLI tool to validate secrets for different services.", "main": "how2validate/index.ts", "scripts": { diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index 7d7975d..e13e77c 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -1,15 +1,13 @@ { "compilerOptions": { "target": "ES6", // JavaScript version to compile to - "module": "ESNext", // Module system (Node.js uses CommonJS) + "module": "CommonJS", // Module system (Node.js uses CommonJS) "outDir": "./dist", // Output directory for compiled JS files "rootDir": "./how2validate", // Directory containing TypeScript source files "strict": true, // Enable all strict type-checking options "esModuleInterop": true, // Enable ES6 import/export interoperability "skipLibCheck": true, // Skip type checking of declaration files - "forceConsistentCasingInFileNames": true, // Ensure consistent file name casing - "moduleResolution": "node", // Ensures Node.js-style resolution - "resolveJsonModule": true // Allows importing .json files directly + "forceConsistentCasingInFileNames": true // Ensure consistent file name casing }, "include": ["how2validate/**/*"] // Specifies which files to include in the project -} +} \ No newline at end of file From 353b36eb2923c56e91f273897686e60c36fcb5ca Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Tue, 24 Sep 2024 00:50:37 +0530 Subject: [PATCH 013/106] docs(comments): updated comments --- .../how2validate/handler/validator_handler.ts | 55 ++-- src/js/how2validate/index.ts | 174 ++++++---- src/js/how2validate/utility/config_utility.ts | 113 ++++--- src/js/how2validate/utility/log_utility.ts | 43 ++- src/js/how2validate/utility/tool_utility.ts | 310 +++++++++++------- .../validators/npm/npm_access_token.ts | 170 +++++++--- .../validators/snyk/snyk_auth_key.ts | 155 ++++++--- .../validators/sonarcloud/sonarcloud_token.ts | 164 ++++++--- src/js/tsconfig.json | 20 +- 9 files changed, 801 insertions(+), 403 deletions(-) diff --git a/src/js/how2validate/handler/validator_handler.ts b/src/js/how2validate/handler/validator_handler.ts index 4269912..4636169 100644 --- a/src/js/how2validate/handler/validator_handler.ts +++ b/src/js/how2validate/handler/validator_handler.ts @@ -1,28 +1,45 @@ -import { validateSnykAuthKey } from '../validators/snyk/snyk_auth_key'; -import { validateSonarcloudToken } from '../validators/sonarcloud/sonarcloud_token'; -import { validateNpmAccessToken } from '../validators/npm/npm_access_token'; +import { validateSnykAuthKey } from "../validators/snyk/snyk_auth_key"; // Import the Snyk authentication key validator +import { validateSonarcloudToken } from "../validators/sonarcloud/sonarcloud_token"; // Import the Sonarcloud token validator +import { validateNpmAccessToken } from "../validators/npm/npm_access_token"; // Import the NPM access token validator -type ValidatorFunction = (service: string, secret: string, response: boolean, report?: boolean) => Promise; +// Define a type for the validator function signature +type ValidatorFunction = ( + service: string, + secret: string, + response: boolean, + report?: boolean +) => Promise; +// Map of service names to their corresponding validator functions const serviceHandlers: Record = { - snyk_auth_key: validateSnykAuthKey, - sonarcloud_token: validateSonarcloudToken, - npm_access_token: validateNpmAccessToken, - // Add all your services here + snyk_auth_key: validateSnykAuthKey, // Snyk auth key validator + sonarcloud_token: validateSonarcloudToken, // Sonarcloud token validator + npm_access_token: validateNpmAccessToken, // NPM access token validator + // Add additional services and their validators here }; +/** + * Handle the validation of a service's secret. + * @param service - The name of the service to validate. + * @param secret - The secret (e.g., API key, token) to validate. + * @param response - A boolean indicating whether to include response data in the output. + * @param report - An optional parameter for additional reporting functionality. + * @returns A promise that resolves to a string message indicating the validation result. + */ export async function validatorHandleService( - service: string, - secret: string, - response: boolean, - report?: boolean + service: string, + secret: string, + response: boolean, + report?: boolean ): Promise { - // Get the handler function based on the service name - const handler = serviceHandlers[service]; + // Retrieve the handler function based on the provided service name + const handler = serviceHandlers[service]; - if (handler) { - return handler(service, secret, response, report); - } else { - return `Error: No handler for service '${service}'`; - } + if (handler) { + // If a handler exists, call it with the provided parameters + return handler(service, secret, response, report); + } else { + // Return an error message if no handler is found for the given service + return `Error: No handler for service '${service}'`; + } } diff --git a/src/js/how2validate/index.ts b/src/js/how2validate/index.ts index a8157bc..7f20e99 100644 --- a/src/js/how2validate/index.ts +++ b/src/js/how2validate/index.ts @@ -1,86 +1,130 @@ -import { Command } from 'commander'; -import { setupLogging } from './utility/log_utility'; +import { Command } from "commander"; // Importing Commander for building CLI applications +import { setupLogging } from "./utility/log_utility"; // Importing the logging setup function import { - getActiveSecretStatus, - getInactiveSecretStatus, - getPackageName, - getVersion -} from './utility/config_utility'; + getActiveSecretStatus, + getInactiveSecretStatus, + getPackageName, + getVersion, +} from "./utility/config_utility"; // Importing configuration utility functions import { - formatServiceProviders, - formatServices, - getSecretProviders, - getSecretServices, - redactSecret, - updateTool, - validateChoice -} from './utility/tool_utility' -import { validatorHandleService } from './handler/validator_handler'; + formatServiceProviders, + formatServices, + getSecretProviders, + getSecretServices, + redactSecret, + updateTool, + validateChoice, +} from "./utility/tool_utility"; // Importing utility functions for secret validation +import { validatorHandleService } from "./handler/validator_handler"; // Importing the validation handler - -// Call the logging setup function +// Call the logging setup function to configure logging setupLogging(); -const program = new Command(); +const program = new Command(); // Create a new Commander program instance +// Configure the CLI program details program - .name('How2Validate Tool') - .description('Validate various types of secrets for different services.') - .version(`How2Validate Tool version ${getVersion() as string}`, '-v, --version', 'Expose the version'); + .name("How2Validate Tool") // Set the name of the CLI tool + .description("Validate various types of secrets for different services.") // Description of what the tool does + .version( + `How2Validate Tool version ${getVersion() as string}`, + "-v, --version", + "Expose the version" + ); // Set the version and help flag -const providerChoices = getSecretProviders(); -const serviceChoices = getSecretServices(); +const providerChoices = getSecretProviders(); // Get supported secret providers +const serviceChoices = getSecretServices(); // Get supported secret services -// Define CLI options using commander +// Define CLI options using Commander program - .option('-provider ', `Secret provider to validate secrets\nSupported providers:\n${formatServiceProviders()}`, (value) => validateChoice(value, providerChoices)) - .option('-service ', `Service / SecretType to validate secrets\nSupported services:\n${formatServices()}`, (value) => validateChoice(value, serviceChoices)) - .option('-secret ', 'Pass the secret to be validated') - .option('-r, --response', `Prints ${getActiveSecretStatus()} / ${getInactiveSecretStatus()} upon validating secrets`) - .option('-report', 'Reports validated secrets over E-mail', false) - .option('--update', 'Hack the tool to the latest version'); + .option( + "-provider ", + `Secret provider to validate secrets\nSupported providers:\n${formatServiceProviders()}`, + (value) => validateChoice(value, providerChoices) + ) // Option for provider + .option( + "-service ", + `Service / SecretType to validate secrets\nSupported services:\n${formatServices()}`, + (value) => validateChoice(value, serviceChoices) + ) // Option for service + .option("-secret ", "Pass the secret to be validated") // Option for secret + .option( + "-r, --response", + `Prints ${getActiveSecretStatus()} / ${getInactiveSecretStatus()} upon validating secrets` + ) // Option for response + .option("-report", "Reports validated secrets over E-mail", false) // Option to report secrets via email + .option("--update", "Hack the tool to the latest version"); // Option to update the tool -async function validate(provider: string, service: string, secret: string, response: boolean, report: boolean) { - console.info("Started validating secret..."); - const result = await validatorHandleService(service, secret, response, report); - console.info(result); +// Function to validate the secret using the specified provider and service +async function validate( + provider: string, + service: string, + secret: string, + response: boolean, + report: boolean +) { + console.info("Started validating secret..."); + const result = await validatorHandleService( + service, + secret, + response, + report + ); // Call the handler for validation + console.info(result); } +// Main function to execute the CLI program logic async function main() { - program.parse(process.argv); + program.parse(process.argv); // Parse command-line arguments - // Convert options keys to lowercase for consistency - const options = Object.fromEntries( - Object.entries(program.opts()).map(([key, value]) => [key.toLowerCase(), value]) - ); - - if (options.update) { - try { - console.info("Initiating tool update..."); - await updateTool(); - console.info("Tool updated successfully."); - return; - } catch (error) { - console.error(`Error during tool update: ${error}`); - return; - } - } - - if (!options.provider || !options.service || !options.secret) { - console.error("Missing required arguments: -provider, -service, -secret"); - console.error("Use '-h' or '--help' for tool usage information."); - // program.help(); - return; - } + // Convert options keys to lowercase for consistency + const options = Object.fromEntries( + Object.entries(program.opts()).map(([key, value]) => [ + key.toLowerCase(), + value, + ]) // Normalize option keys + ); + // Check for the update option + if (options.update) { try { - console.info(`Initiating validation for service: ${options.service} with secret: ${redactSecret(options.secret)}`); - await validate(options.provider, options.service, options.secret, options.response, options.report); - console.info("Validation completed successfully."); + console.info("Initiating tool update..."); + await updateTool(); // Call the update function + console.info("Tool updated successfully."); + return; // Exit after updating } catch (error) { - console.error(`An error occurred during validation: ${error}`); - console.log(`Error: ${error}`); + console.error(`Error during tool update: ${error}`); // Log any errors + return; } + } + + // Validate required arguments + if (!options.provider || !options.service || !options.secret) { + console.error("Missing required arguments: -provider, -service, -secret"); + console.error("Use '-h' or '--help' for tool usage information."); // Provide help info + return; + } + + // Attempt to validate the secret + try { + console.info( + `Initiating validation for service: ${ + options.service + } with secret: ${redactSecret(options.secret)}` + ); + await validate( + options.provider, + options.service, + options.secret, + options.response, + options.report + ); // Call validate function + console.info("Validation completed successfully."); + } catch (error) { + console.error(`An error occurred during validation: ${error}`); // Handle validation errors + console.log(`Error: ${error}`); + } } -main().catch(error => console.error(`Unexpected error: ${error}`)); +// Start the main function and handle any unexpected errors +main().catch((error) => console.error(`Unexpected error: ${error}`)); diff --git a/src/js/how2validate/utility/config_utility.ts b/src/js/how2validate/utility/config_utility.ts index 98eb7b6..b221b6f 100644 --- a/src/js/how2validate/utility/config_utility.ts +++ b/src/js/how2validate/utility/config_utility.ts @@ -1,78 +1,103 @@ -import * as path from 'path'; -import * as fs from 'fs'; -import * as ini from 'ini'; +import * as path from "path"; // Importing path module for handling file and directory paths +import * as fs from "fs"; // Importing fs module for file system operations +import * as ini from "ini"; // Importing ini module for parsing .ini configuration files +// Define an interface for the configuration structure interface Config { DEFAULT?: { - package_name?: string; - version?: string; + package_name?: string; // Package name from the DEFAULT section + version?: string; // Version from the DEFAULT section }; SECRET?: { - secret_active?: string; - secret_inactive?: string; + secret_active?: string; // Active secret status from the SECRET section + secret_inactive?: string; // Inactive secret status from the SECRET section }; } +// Variable to hold the configuration let config: Config | null = null; +/** + * Get the path to the configuration file based on the environment. + * @returns The path to the config.ini file. + */ function getConfigFilePath() { - if (process.env.NODE_ENV === 'production') { - // Assuming your config.ini is in the dist folder when published - return path.resolve(__dirname, '..', 'config.ini'); - } else { - // For local development - return path.resolve(__dirname, '..', '..', 'config.ini'); - } + if (process.env.NODE_ENV === "production") { + // Path for production environment, assumes config.ini is in the dist folder + return path.resolve(__dirname, "..", "config.ini"); + } else { + // Path for local development environment + return path.resolve(__dirname, "..", "..", "config.ini"); } - +} + +/** + * Initialize the configuration by reading the config.ini file. + */ export function initConfig() { - // Path to the config.ini file, relative to where your compiled JavaScript will be running (in dist) - const configFilePath = getConfigFilePath(); - //const configFilePath = path.resolve(__dirname, '..', '..', 'config.ini'); // Adjusted for dist directory + const configFilePath = getConfigFilePath(); // Get the path to the config.ini file try { - const configContent = fs.readFileSync(configFilePath, 'utf-8'); - config = ini.parse(configContent); // Parse the .ini file + const configContent = fs.readFileSync(configFilePath, "utf-8"); // Read the file content + config = ini.parse(configContent); // Parse the .ini file content into a JavaScript object } catch (error) { - console.error(`Error: The file '${configFilePath}' was not found or could not be read.`); + console.error( + `Error: The file '${configFilePath}' was not found or could not be read.` + ); // Log error if file cannot be read } } -// Function to get the package name from the DEFAULT section +/** + * Get the package name from the DEFAULT section of the config. + * @returns The package name or undefined if not set. + * @throws An error if the configuration is not initialized. + */ export function getPackageName(): string | undefined { if (config && config.DEFAULT) { - return config.DEFAULT.package_name as string; + return config.DEFAULT.package_name as string; // Return package name } else { - throw new Error("Configuration not initialized. Call initConfig() first."); + throw new Error("Configuration not initialized. Call initConfig() first."); // Error if config not initialized } } -// Function to get the active secret status from the SECRET section +/** + * Get the active secret status from the SECRET section of the config. + * @returns The active secret status or undefined if not set. + * @throws An error if the configuration is not initialized. + */ export function getActiveSecretStatus(): string | undefined { - if (config && config.SECRET) { - return config.SECRET.secret_active as string; - } else { - throw new Error("Configuration not initialized. Call initConfig() first."); - } + if (config && config.SECRET) { + return config.SECRET.secret_active as string; // Return active secret status + } else { + throw new Error("Configuration not initialized. Call initConfig() first."); // Error if config not initialized + } } - -// Function to get the inactive secret status from the SECRET section + +/** + * Get the inactive secret status from the SECRET section of the config. + * @returns The inactive secret status or undefined if not set. + * @throws An error if the configuration is not initialized. + */ export function getInactiveSecretStatus(): string | undefined { - if (config && config.SECRET) { - return config.SECRET.secret_inactive as string; - } else { - throw new Error("Configuration not initialized. Call initConfig() first."); - } + if (config && config.SECRET) { + return config.SECRET.secret_inactive as string; // Return inactive secret status + } else { + throw new Error("Configuration not initialized. Call initConfig() first."); // Error if config not initialized + } } -// Function to get the version from the DEFAULT section +/** + * Get the version from the DEFAULT section of the config. + * @returns The version or undefined if not set. + * @throws An error if the configuration is not initialized. + */ export function getVersion(): string | undefined { - if (config && config.DEFAULT) { - return config.DEFAULT.version as string; - } else { - throw new Error("Configuration not initialized. Call initConfig() first."); - } + if (config && config.DEFAULT) { + return config.DEFAULT.version as string; // Return version + } else { + throw new Error("Configuration not initialized. Call initConfig() first."); // Error if config not initialized + } } -// Call initConfig when this file is imported or used +// Automatically call initConfig when this module is imported to ensure configuration is loaded initConfig(); diff --git a/src/js/how2validate/utility/log_utility.ts b/src/js/how2validate/utility/log_utility.ts index a27ee9a..8f7d55a 100644 --- a/src/js/how2validate/utility/log_utility.ts +++ b/src/js/how2validate/utility/log_utility.ts @@ -1,29 +1,54 @@ -import * as logging from 'loglevel'; -import { getActiveSecretStatus, getInactiveSecretStatus } from './config_utility'; +import * as logging from "loglevel"; // Importing the loglevel library for logging functionality +import { + getActiveSecretStatus, + getInactiveSecretStatus, +} from "./config_utility"; // Importing utility functions to get secret status values +/** + * Set up the logging configuration. + * This function initializes the logging level to INFO to control the verbosity of log messages. + */ export function setupLogging() { logging.setLevel("INFO"); // Set logging level to INFO } -export function getSecretStatusMessage(service: string, isActive: string, response?: boolean, responseData?: any): string { +/** + * Generate a message about the status of a secret. + * @param service - The name of the service associated with the secret. + * @param isActive - The current status of the secret (active or inactive). + * @param response - Optional parameter to include additional response data. + * @param responseData - Optional data to provide more context if a response exists. + * @returns A formatted message describing the secret's status. + * @throws An error if the isActive value is not recognized. + */ +export function getSecretStatusMessage( + service: string, + isActive: string, + response?: boolean, + responseData?: any +): string { // Normalize isActive values to handle both 'Active' and 'InActive' let status: string; - + + // Check if the secret is active or inactive based on provided status if (isActive === getActiveSecretStatus()) { - status = "active and operational"; + status = "active and operational"; // Set status message for active secret } else if (isActive === getInactiveSecretStatus()) { - status = "inactive and not operational"; + status = "inactive and not operational"; // Set status message for inactive secret } else { - throw new Error(`Unexpected isActive value: ${isActive}. Expected 'Active' or 'InActive'.`); + // Throw an error for unexpected isActive values + throw new Error( + `Unexpected isActive value: ${isActive}. Expected 'Active' or 'InActive'.` + ); } // Base message about the secret's status let message = `The provided secret '${service}' is currently ${status}.`; - + // If a response exists, append it to the message if (response) { message += ` Here is the additional response data:\n${responseData}`; } - return message; + return message; // Return the formatted status message } diff --git a/src/js/how2validate/utility/tool_utility.ts b/src/js/how2validate/utility/tool_utility.ts index d0cdf6e..bf80420 100644 --- a/src/js/how2validate/utility/tool_utility.ts +++ b/src/js/how2validate/utility/tool_utility.ts @@ -1,165 +1,237 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as logging from 'loglevel'; -import { execSync } from 'child_process'; +import * as fs from "fs"; // Importing the 'fs' module for file system operations +import * as path from "path"; // Importing the 'path' module for handling file and directory paths +import * as logging from "loglevel"; // Importing loglevel for logging messages +import { execSync } from "child_process"; // Importing execSync to run shell commands synchronously -import { getPackageName } from './config_utility'; -import { setupLogging } from './log_utility'; +import { getPackageName } from "./config_utility"; // Importing a function to get the package name from configuration +import { setupLogging } from "./log_utility"; // Importing the function to set up logging -// Call the logging setup function +// Call the logging setup function to initialize logging configuration setupLogging(); // Get the directory of the current file const currentDir = __dirname; -// Path to the TokenManager JSON file -const tokenManager_filePath = path.join(currentDir, '..', '..', 'tokenManager.json'); - -export function getSecretProviders(filePath: string = tokenManager_filePath): string[] { - const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); - const enabledSecretsServices: string[] = []; - - for (const provider in data) { - const tokens = data[provider]; - for (const tokenInfo of tokens) { - if (tokenInfo.is_enabled) { - enabledSecretsServices.push(provider); - } - } +// Define the path to the TokenManager JSON file +const tokenManager_filePath = path.join( + currentDir, + "..", + "..", + "tokenManager.json" +); + +/** + * Get the secret providers from the TokenManager JSON file. + * @param filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). + * @returns An array of enabled secret providers. + */ +export function getSecretProviders( + filePath: string = tokenManager_filePath +): string[] { + // Read the JSON file and parse its contents + const data = JSON.parse(fs.readFileSync(filePath, "utf-8")); + const enabledSecretsServices: string[] = []; + + // Iterate through each provider in the data + for (const provider in data) { + const tokens = data[provider]; + // Check each token's enabled status + for (const tokenInfo of tokens) { + if (tokenInfo.is_enabled) { + enabledSecretsServices.push(provider); // Add enabled provider to the list + } } - return enabledSecretsServices; + } + return enabledSecretsServices; // Return the list of enabled secret providers } -export function getSecretServices(filePath: string = tokenManager_filePath): string[] { - const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); - const enabledSecretsServices: string[] = []; - - for (const provider in data) { - const tokens = data[provider]; - for (const tokenInfo of tokens) { - if (tokenInfo.is_enabled) { - enabledSecretsServices.push(tokenInfo.display_name); - } - } +/** + * Get the secret services from the TokenManager JSON file. + * @param filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). + * @returns An array of enabled secret services. + */ +export function getSecretServices( + filePath: string = tokenManager_filePath +): string[] { + const data = JSON.parse(fs.readFileSync(filePath, "utf-8")); + const enabledSecretsServices: string[] = []; + + for (const provider in data) { + const tokens = data[provider]; + for (const tokenInfo of tokens) { + if (tokenInfo.is_enabled) { + enabledSecretsServices.push(tokenInfo.display_name); // Add display name of enabled services + } } + } - return enabledSecretsServices; + return enabledSecretsServices; // Return the list of enabled secret services } -export function formatServiceProviders(filePath: string = tokenManager_filePath): string { - const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); - const enabledSecretsServices: string[] = []; - - for (const provider in data) { - const tokens = data[provider]; - for (const tokenInfo of tokens) { - if (tokenInfo.is_enabled) { - enabledSecretsServices.push(provider); - } - } +/** + * Format the enabled service providers for display. + * @param filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). + * @returns A formatted string of enabled service providers. + */ +export function formatServiceProviders( + filePath: string = tokenManager_filePath +): string { + const data = JSON.parse(fs.readFileSync(filePath, "utf-8")); + const enabledSecretsServices: string[] = []; + + for (const provider in data) { + const tokens = data[provider]; + for (const tokenInfo of tokens) { + if (tokenInfo.is_enabled) { + enabledSecretsServices.push(provider); // Add enabled provider to the list + } } + } - return enabledSecretsServices.map(service => ` - ${service}`).join('\n'); + // Format each service provider with a bullet point and join them into a single string + return enabledSecretsServices.map((service) => ` - ${service}`).join("\n"); } -export function formatServices(filePath: string = tokenManager_filePath): string { - const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); - const enabledSecretsServices: string[] = []; - - for (const provider in data) { - const tokens = data[provider]; - for (const tokenInfo of tokens) { - if (tokenInfo.is_enabled) { - enabledSecretsServices.push(`${provider} - ${tokenInfo.display_name}`); - } - } +/** + * Format the enabled services for display. + * @param filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). + * @returns A formatted string of enabled services with their providers. + */ +export function formatServices( + filePath: string = tokenManager_filePath +): string { + const data = JSON.parse(fs.readFileSync(filePath, "utf-8")); + const enabledSecretsServices: string[] = []; + + for (const provider in data) { + const tokens = data[provider]; + for (const tokenInfo of tokens) { + if (tokenInfo.is_enabled) { + // Add formatted string of provider and service display name + enabledSecretsServices.push(`${provider} - ${tokenInfo.display_name}`); + } } + } - return enabledSecretsServices.map(service => ` - ${service}`).join('\n'); + // Format each service with a bullet point and join them into a single string + return enabledSecretsServices.map((service) => ` - ${service}`).join("\n"); } +/** + * Format a string by converting it to lowercase and replacing spaces with underscores. + * @param inputString - The string to format. + * @returns The formatted string. + * @throws An error if the input is not a string. + */ export function formatString(inputString: string): string { - if (typeof inputString !== 'string') { - throw new Error("Input must be a string"); - } - - return inputString.toLowerCase().replace(/ /g, '_'); + if (typeof inputString !== "string") { + throw new Error("Input must be a string"); // Ensure input is a string + } + + return inputString.toLowerCase().replace(/ /g, "_"); // Format the string } +/** + * Validate a user's choice against a list of valid choices. + * @param value - The value to validate. + * @param validChoices - An array of valid choices. + * @returns The formatted value if valid. + * @throws An error if the value is not valid. + */ export function validateChoice(value: string, validChoices: string[]): string { - const formattedValue = formatString(value); - const formattedChoices = validChoices.map(choice => formatString(choice)); + const formattedValue = formatString(value); // Format the user's choice + const formattedChoices = validChoices.map((choice) => formatString(choice)); // Format valid choices - if (!formattedChoices.includes(formattedValue)) { - throw new Error(`Invalid choice: '${value}'. Choose from ${validChoices.join(', ')}.`); - } + if (!formattedChoices.includes(formattedValue)) { + // Throw an error if the choice is invalid + throw new Error( + `Invalid choice: '${value}'. Choose from ${validChoices.join(", ")}.` + ); + } - return formattedValue; + return formattedValue; // Return the formatted value } +/** + * Redact a secret by masking part of it with asterisks. + * @param secret - The secret to redact. + * @returns The redacted secret. + * @throws An error if the input is not a string. + */ export function redactSecret(secret: string): string { - if (typeof secret !== 'string') { - throw new Error("Input must be a string"); - } + if (typeof secret !== "string") { + throw new Error("Input must be a string"); // Ensure input is a string + } - if (secret.length <= 5) { - return secret; // Return the secret as is if it's 5 characters or less - } + if (secret.length <= 5) { + return secret; // Return the secret as is if it's 5 characters or less + } - return secret.slice(0, 5) + '*'.repeat(secret.length - 5); + // Redact the secret by keeping the first 5 characters and masking the rest + return secret.slice(0, 5) + "*".repeat(secret.length - 5); } +/** + * Update the tool to the latest version using the appropriate package manager. + */ export function updateTool(): void { - logging.info("Updating the tool..."); - - // Determine the package manager - const packageManager = detectPackageManager(); - const packageName = getPackageName(); - - try { - switch (packageManager) { - case 'npm': - execSync(`npm install --global ${packageName}`, { stdio: 'inherit' }); - break; - case 'yarn': - execSync(`yarn global add ${packageName}`, { stdio: 'inherit' }); - break; - case 'pnpm': - execSync(`pnpm add --global ${packageName}`, { stdio: 'inherit' }); - break; - case 'bun': - execSync(`bun add ${packageName} --global`, { stdio: 'inherit' }); - break; - default: - console.error("Unsupported package manager. Please install the tool manually."); - return; - } - console.log("Tool updated to the latest version."); - } catch (error) { - console.error(`Failed to update the tool: ${error}`); + logging.info("Updating the tool..."); // Log the update initiation + + // Determine the package manager being used + const packageManager = detectPackageManager(); + const packageName = getPackageName(); // Get the package name + + try { + // Execute the appropriate command based on the package manager + switch (packageManager) { + case "npm": + execSync(`npm install --global ${packageName}`, { stdio: "inherit" }); + break; + case "yarn": + execSync(`yarn global add ${packageName}`, { stdio: "inherit" }); + break; + case "pnpm": + execSync(`pnpm add --global ${packageName}`, { stdio: "inherit" }); + break; + case "bun": + execSync(`bun add ${packageName} --global`, { stdio: "inherit" }); + break; + default: + console.error( + "Unsupported package manager. Please install the tool manually." + ); + return; // Exit if the package manager is not recognized } + console.log("Tool updated to the latest version."); // Log success message + } catch (error) { + console.error(`Failed to update the tool: ${error}`); // Log error message if the update fails + } } +/** + * Detect the package manager installed on the system. + * @returns The name of the package manager (npm, yarn, pnpm, bun, or unknown). + */ function detectPackageManager(): string { + try { + execSync("npm --version", { stdio: "ignore" }); // Check for npm + return "npm"; + } catch { try { - execSync('npm --version', { stdio: 'ignore' }); - return 'npm'; + execSync("yarn --version", { stdio: "ignore" }); // Check for yarn + return "yarn"; } catch { + try { + execSync("pnpm --version", { stdio: "ignore" }); // Check for pnpm + return "pnpm"; + } catch { try { - execSync('yarn --version', { stdio: 'ignore' }); - return 'yarn'; + execSync("bun --version", { stdio: "ignore" }); // Check for bun + return "bun"; } catch { - try { - execSync('pnpm --version', { stdio: 'ignore' }); - return 'pnpm'; - } catch { - try { - execSync('bun --version', { stdio: 'ignore' }); - return 'bun'; - } catch { - return 'unknown'; // For unrecognized package managers - } - } + return "unknown"; // Return 'unknown' if no recognized package manager is found } + } } -} \ No newline at end of file + } +} diff --git a/src/js/how2validate/validators/npm/npm_access_token.ts b/src/js/how2validate/validators/npm/npm_access_token.ts index a2ddbcf..ea56c23 100644 --- a/src/js/how2validate/validators/npm/npm_access_token.ts +++ b/src/js/how2validate/validators/npm/npm_access_token.ts @@ -1,57 +1,143 @@ -import axios, { AxiosError } from 'axios'; -import { getActiveSecretStatus, getInactiveSecretStatus } from '../../utility/config_utility'; -import { getSecretStatusMessage } from '../../utility/log_utility'; +import axios, { AxiosError } from "axios"; // Import Axios for making HTTP requests and the AxiosError type for error handling +import { + getActiveSecretStatus, + getInactiveSecretStatus, +} from "../../utility/config_utility"; // Import functions to retrieve active and inactive secret statuses +import { getSecretStatusMessage } from "../../utility/log_utility"; // Import function to format status messages +/** + * Validate an NPM access token by making an API request to check its validity. + * @param service - The name of the service being validated (in this case, NPM). + * @param secret - The NPM access token to validate. + * @param response - A flag to indicate whether to include detailed response data. + * @param report - An optional flag for additional reporting features. + * @returns A promise that resolves to a status message about the access token validation. + */ export async function validateNpmAccessToken( - service: string, - secret: string, - response: boolean, - report?: boolean + service: string, + secret: string, + response: boolean, + report?: boolean ): Promise { - const url = "https://registry.npmjs.org/-/npm/v1/user"; - const nocacheHeaders = { 'Cache-Control': 'no-cache' }; - const headers = { 'Authorization': `Bearer ${secret}` }; + // NPM API endpoint for retrieving user information + const url = "https://registry.npmjs.org/-/npm/v1/user"; - try { - const responseData = await axios.get(url, { - headers: { ...nocacheHeaders, ...headers } - }); + // Headers to prevent caching and to authorize the request using the provided token + const nocacheHeaders = { "Cache-Control": "no-cache" }; + const headers = { Authorization: `Bearer ${secret}` }; - if (responseData.status === 200) { - if (!response) { - return getSecretStatusMessage(service, getActiveSecretStatus() as string, response); - } else { - return getSecretStatusMessage(service, getActiveSecretStatus() as string, response, JSON.stringify(responseData.data, null, 4)); - } - } else { - return handleInactiveStatus(service, response, responseData.data); - } - } catch (error) { - return handleErrors(error, service, response); - } -} + try { + // Send a GET request to the NPM API to validate the access token + const responseData = await axios.get(url, { + headers: { ...nocacheHeaders, ...headers }, + }); -function handleInactiveStatus(service: string, response: boolean, data?: any): string { - if (!response) { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + // Check if the request was successful (HTTP 200) + if (responseData.status === 200) { + // If no detailed response is requested, return the active status message + if (!response) { + return getSecretStatusMessage( + service, + getActiveSecretStatus() as string, + response + ); + } else { + // Return the active status message along with the response data + return getSecretStatusMessage( + service, + getActiveSecretStatus() as string, + response, + JSON.stringify(responseData.data, null, 4) + ); + } } else { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, data ? JSON.stringify(data) : "No additional data."); + // If the response status code is not 200, handle it as an inactive token + return handleInactiveStatus(service, response, responseData.data); } + } catch (error) { + // Handle errors that occur during the request and return the inactive status message + return handleErrors(error, service, response); + } } -function handleErrors(error: unknown, service: string, response: boolean): string { - if (axios.isAxiosError(error)) { - const axiosError = error as AxiosError; - if (!response) { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); - } else { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, axiosError.response?.data || axiosError.message); - } - } +/** + * Handle the case when the NPM access token is inactive or invalid. + * @param service - The name of the service being validated (NPM). + * @param response - A flag to indicate whether to include detailed response data. + * @param data - Optional additional data to include in the response. + * @returns A formatted status message indicating the inactive status. + */ +function handleInactiveStatus( + service: string, + response: boolean, + data?: any +): string { + if (!response) { + // Return inactive status message without additional data + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response + ); + } else { + // Return inactive status message with additional response data + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response, + data ? JSON.stringify(data) : "No additional data." + ); + } +} +/** + * Handle errors that occur during the validation process. + * @param error - The error object that was thrown during the request. + * @param service - The name of the service being validated (NPM). + * @param response - A flag to indicate whether to include detailed response data. + * @returns A formatted status message based on the type of error encountered. + */ +function handleErrors( + error: unknown, + service: string, + response: boolean +): string { + // Check if the error is an Axios error + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; // Cast to AxiosError type if (!response) { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + // Return inactive status message without additional error details + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response + ); } else { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, "An unexpected error occurred."); + // Return inactive status message with error details from the response or message + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response, + axiosError.response?.data || axiosError.message + ); } + } + + // Handle general errors that are not Axios errors + if (!response) { + // Return inactive status message without additional error details + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response + ); + } else { + // Return inactive status message with a general error message + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response, + "An unexpected error occurred." + ); + } } diff --git a/src/js/how2validate/validators/snyk/snyk_auth_key.ts b/src/js/how2validate/validators/snyk/snyk_auth_key.ts index b3f7af3..fabfa87 100644 --- a/src/js/how2validate/validators/snyk/snyk_auth_key.ts +++ b/src/js/how2validate/validators/snyk/snyk_auth_key.ts @@ -1,63 +1,116 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import axios from 'axios'; -import { getActiveSecretStatus, getInactiveSecretStatus } from '../../utility/config_utility'; -import { getSecretStatusMessage } from '../../utility/log_utility'; +import * as fs from "fs"; // Import the file system module for file operations +import * as path from "path"; // Import the path module for handling file paths +import axios from "axios"; // Import Axios for making HTTP requests +import { + getActiveSecretStatus, + getInactiveSecretStatus, +} from "../../utility/config_utility"; // Import functions to get secret statuses +import { getSecretStatusMessage } from "../../utility/log_utility"; // Import function to format status messages +/** + * Validate a Snyk authentication key by making an API request to check its validity. + * @param service - The name of the service being validated. + * @param secret - The Snyk API key (token) to validate. + * @param response - A flag to indicate whether to include detailed response data. + * @param report - An optional flag for additional reporting features. + * @returns A promise that resolves to a status message about the authentication key validation. + */ export async function validateSnykAuthKey( - service: string, - secret: string, - response: boolean, - report?: boolean + service: string, + secret: string, + response: boolean, + report?: boolean ): Promise { - // Snyk API endpoint for getting user information - const url = 'https://snyk.io/api/v1/user'; - - // Headers to ensure no caching and to authorize the request using the provided API key (token) - const headers = { - 'Cache-Control': 'no-cache', - Authorization: `token ${secret}`, - }; - - try { - // Send a GET request to the Snyk API - const responseData = await axios.get(url, { headers }); - - // If the request was successful (HTTP 200) - if (responseData.status === 200) { - // If `response` is false, return the active status message without response data - if (!response) { - return getSecretStatusMessage(service, getActiveSecretStatus() as string, response); - } else { - // Return the status message along with the response data - return getSecretStatusMessage(service, getActiveSecretStatus() as string, response, JSON.stringify(responseData.data, null, 4)); - } - } else { - // If the response status code is not 200, treat the secret as inactive - return handleInactiveResponse(service, response, responseData.data); - } - } catch (error) { - // Handle errors and return the inactive status message - return handleError(service, response, error); - } -} + // Snyk API endpoint for retrieving user information + const url = "https://snyk.io/api/v1/user"; + + // Headers to prevent caching and authorize the request using the provided API key (token) + const headers = { + "Cache-Control": "no-cache", + Authorization: `token ${secret}`, // Authorization header with the Snyk token + }; + + try { + // Send a GET request to the Snyk API to validate the token + const responseData = await axios.get(url, { headers }); -function handleInactiveResponse(service: string, response: boolean, responseData: any): string { - if (!response) { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + // Check if the request was successful (HTTP 200) + if (responseData.status === 200) { + // If no detailed response is requested, return the active status message + if (!response) { + return getSecretStatusMessage( + service, + getActiveSecretStatus() as string, + response + ); + } else { + // Return the active status message along with the response data + return getSecretStatusMessage( + service, + getActiveSecretStatus() as string, + response, + JSON.stringify(responseData.data, null, 4) + ); + } } else { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, responseData); + // If the response status code is not 200, handle it as an inactive key + return handleInactiveResponse(service, response, responseData.data); } + } catch (error) { + // Handle errors that occur during the request and return the inactive status message + return handleError(service, response, error); + } } +/** + * Handle the case when the authentication key is inactive or invalid. + * @param service - The name of the service being validated. + * @param response - A flag to indicate whether to include detailed response data. + * @param responseData - Optional additional data to include in the response. + * @returns A formatted status message indicating the inactive status. + */ +function handleInactiveResponse( + service: string, + response: boolean, + responseData: any +): string { + if (!response) { + // Return inactive status message without additional data + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response + ); + } else { + // Return inactive status message with additional response data + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response, + responseData + ); + } +} + +/** + * Handle errors that occur during the validation process. + * @param service - The name of the service being validated. + * @param response - A flag to indicate whether to include detailed response data. + * @param error - The error object that was thrown. + * @returns A formatted status message based on the type of error encountered. + */ function handleError(service: string, response: boolean, error: any): string { - let errorMessage = ''; + let errorMessage = ""; - if (axios.isAxiosError(error)) { - errorMessage = error.response ? error.response.data : error.message; - } else { - errorMessage = error.message; - } + // Check if the error is an Axios error + if (axios.isAxiosError(error)) { + // Extract the error message or response data + errorMessage = error.response ? error.response.data : error.message; + } else { + // Handle general errors that are not Axios errors + errorMessage = error.message; + } - return handleInactiveResponse(service, response, errorMessage); + // Return inactive response handling with the error message + return handleInactiveResponse(service, response, errorMessage); } diff --git a/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts b/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts index bc00eda..15a9b84 100644 --- a/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts +++ b/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts @@ -1,58 +1,134 @@ -import axios, { AxiosError } from 'axios'; - -import { getActiveSecretStatus, getInactiveSecretStatus } from '../../utility/config_utility'; -import { getSecretStatusMessage } from '../../utility/log_utility'; +import axios, { AxiosError } from "axios"; // Import Axios for making HTTP requests and AxiosError for error handling +import { + getActiveSecretStatus, + getInactiveSecretStatus, +} from "../../utility/config_utility"; // Import functions to get secret statuses +import { getSecretStatusMessage } from "../../utility/log_utility"; // Import function to format status messages +/** + * Validate a SonarCloud token by making an API request to check its validity. + * @param service - The name of the service being validated. + * @param secret - The token or secret to validate. + * @param response - A flag to indicate whether to include detailed response data. + * @param report - An optional flag for additional reporting features. + * @returns A promise that resolves to a status message about the token validation. + */ export async function validateSonarcloudToken( - service: string, - secret: string, - response: boolean, - report?: boolean + service: string, + secret: string, + response: boolean, + report?: boolean ): Promise { - const url = "https://sonarcloud.io/api/users/current"; - const nocacheHeaders = { 'Cache-Control': 'no-cache' }; - const headers = { 'Authorization': `Bearer ${secret}` }; - - try { - const responseData = await axios.get(url, { - headers: { ...nocacheHeaders, ...headers } - }); + const url = "https://sonarcloud.io/api/users/current"; // API endpoint for SonarCloud user information + const nocacheHeaders = { "Cache-Control": "no-cache" }; // Header to prevent caching + const headers = { Authorization: `Bearer ${secret}` }; // Authorization header with the Bearer token - if (responseData.status === 200) { - if (!response) { - return getSecretStatusMessage(service, getActiveSecretStatus() as string, response); - } else { - return getSecretStatusMessage(service, getActiveSecretStatus() as string, response, JSON.stringify(responseData.data, null, 4)); - } - } else { - return handleInactiveStatus(service, response, responseData.data); - } - } catch (error) { - return handleErrors(error, service, response); - } -} + try { + // Make the API request to validate the token + const responseData = await axios.get(url, { + headers: { ...nocacheHeaders, ...headers }, // Combine headers + }); -function handleInactiveStatus(service: string, response: boolean, data?: any): string { - if (!response) { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + // Check if the request was successful + if (responseData.status === 200) { + // If successful and no detailed response is requested + if (!response) { + return getSecretStatusMessage( + service, + getActiveSecretStatus() as string, + response + ); + } else { + // If successful and detailed response is requested + return getSecretStatusMessage( + service, + getActiveSecretStatus() as string, + response, + JSON.stringify(responseData.data, null, 4) + ); + } } else { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, data ? JSON.stringify(data) : "No additional data."); + // Handle non-successful status codes + return handleInactiveStatus(service, response, responseData.data); } + } catch (error) { + // Handle errors that occur during the request + return handleErrors(error, service, response); + } } -function handleErrors(error: unknown, service: string, response: boolean): string { - if (axios.isAxiosError(error)) { - const axiosError = error as AxiosError; - if (!response) { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); - } else { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, axiosError.response?.data || axiosError.message); - } - } +/** + * Handle the case when the token is inactive or invalid. + * @param service - The name of the service being validated. + * @param response - A flag to indicate whether to include detailed response data. + * @param data - Optional additional data to include in the response. + * @returns A formatted status message indicating the inactive status. + */ +function handleInactiveStatus( + service: string, + response: boolean, + data?: any +): string { + if (!response) { + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response + ); + } else { + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response, + data ? JSON.stringify(data) : "No additional data." + ); + } +} +/** + * Handle errors that occur during the validation process. + * @param error - The error object that was thrown. + * @param service - The name of the service being validated. + * @param response - A flag to indicate whether to include detailed response data. + * @returns A formatted status message based on the type of error. + */ +function handleErrors( + error: unknown, + service: string, + response: boolean +): string { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; // Cast error to AxiosError for more specific handling if (!response) { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response); + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response + ); } else { - return getSecretStatusMessage(service, getInactiveSecretStatus() as string, response, "An unexpected error occurred."); + // Include detailed error information in the response + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response, + axiosError.response?.data || axiosError.message + ); } + } + + // Handle general errors that are not Axios errors + if (!response) { + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response + ); + } else { + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response, + "An unexpected error occurred." + ); + } } diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index e13e77c..2c30a1b 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -1,13 +1,13 @@ { "compilerOptions": { - "target": "ES6", // JavaScript version to compile to - "module": "CommonJS", // Module system (Node.js uses CommonJS) - "outDir": "./dist", // Output directory for compiled JS files - "rootDir": "./how2validate", // Directory containing TypeScript source files - "strict": true, // Enable all strict type-checking options - "esModuleInterop": true, // Enable ES6 import/export interoperability - "skipLibCheck": true, // Skip type checking of declaration files - "forceConsistentCasingInFileNames": true // Ensure consistent file name casing + "target": "ES6", // Specifies the JavaScript version to compile to. ES6 (also known as ES2015) is a widely supported version. + "module": "CommonJS", // Defines the module system to use. CommonJS is the standard for Node.js modules. + "outDir": "./dist", // The output directory where compiled JavaScript files will be placed. + "rootDir": "./how2validate", // The root directory of your TypeScript source files; all files under this directory will be included in the compilation. + "strict": true, // Enables all strict type-checking options for more rigorous checks. + "esModuleInterop": true, // Allows default imports from modules with no default export. Enables interoperability between CommonJS and ES modules. + "skipLibCheck": true, // Skips type checking of declaration files (e.g., `.d.ts` files), speeding up the compilation process. + "forceConsistentCasingInFileNames": true // Ensures that file name casing is consistent across the project to prevent errors on case-sensitive file systems. }, - "include": ["how2validate/**/*"] // Specifies which files to include in the project -} \ No newline at end of file + "include": ["how2validate/**/*"] // Specifies which files to include in the project; here, it includes all files in the `how2validate` directory and its subdirectories. +} From 607ff966e0ea7126ba44567bf2e6c234e8274dba Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Tue, 24 Sep 2024 00:52:44 +0530 Subject: [PATCH 014/106] fix(test): updated test files --- tests/js/test_validator.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/js/test_validator.js diff --git a/tests/js/test_validator.js b/tests/js/test_validator.js deleted file mode 100644 index e69de29..0000000 From 0d600a281a0f9f74f993b4eecf9a8f7c9f1b2cf8 Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Wed, 25 Sep 2024 12:29:15 +0530 Subject: [PATCH 015/106] fix(jsr): exporting function --- src/js/how2validate/index.ts | 8 ++++---- src/js/jsr.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/js/how2validate/index.ts b/src/js/how2validate/index.ts index 7f20e99..c71bee8 100644 --- a/src/js/how2validate/index.ts +++ b/src/js/how2validate/index.ts @@ -38,16 +38,16 @@ const serviceChoices = getSecretServices(); // Get supported secret services // Define CLI options using Commander program .option( - "-provider ", + "-provider ", `Secret provider to validate secrets\nSupported providers:\n${formatServiceProviders()}`, (value) => validateChoice(value, providerChoices) ) // Option for provider .option( - "-service ", + "-service ", `Service / SecretType to validate secrets\nSupported services:\n${formatServices()}`, (value) => validateChoice(value, serviceChoices) ) // Option for service - .option("-secret ", "Pass the secret to be validated") // Option for secret + .option("-secret ", "Pass the secret to be validated") // Option for secret .option( "-r, --response", `Prints ${getActiveSecretStatus()} / ${getInactiveSecretStatus()} upon validating secrets` @@ -56,7 +56,7 @@ program .option("--update", "Hack the tool to the latest version"); // Option to update the tool // Function to validate the secret using the specified provider and service -async function validate( +export async function validate( provider: string, service: string, secret: string, diff --git a/src/js/jsr.json b/src/js/jsr.json index b40592d..e88eccf 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.2-beta.3", + "version": "0.0.2-beta.4", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { From 8b69040e367e632d790805bf18a7f90bd03c8431 Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Wed, 25 Sep 2024 17:42:56 +0530 Subject: [PATCH 016/106] fix(jsr): updated path --- src/js/how2validate/utility/config_utility.ts | 108 ++++++++---------- src/js/jsr.json | 2 +- src/js/package.json | 10 +- src/js/tsconfig.json | 20 ++-- 4 files changed, 67 insertions(+), 73 deletions(-) diff --git a/src/js/how2validate/utility/config_utility.ts b/src/js/how2validate/utility/config_utility.ts index b221b6f..a319ebb 100644 --- a/src/js/how2validate/utility/config_utility.ts +++ b/src/js/how2validate/utility/config_utility.ts @@ -1,103 +1,93 @@ -import * as path from "path"; // Importing path module for handling file and directory paths -import * as fs from "fs"; // Importing fs module for file system operations -import * as ini from "ini"; // Importing ini module for parsing .ini configuration files +import * as path from "path"; +import * as fs from "fs"; -// Define an interface for the configuration structure interface Config { DEFAULT?: { - package_name?: string; // Package name from the DEFAULT section - version?: string; // Version from the DEFAULT section + package_name?: string; + version?: string; }; SECRET?: { - secret_active?: string; // Active secret status from the SECRET section - secret_inactive?: string; // Inactive secret status from the SECRET section + secret_active?: string; + secret_inactive?: string; }; } -// Variable to hold the configuration let config: Config | null = null; -/** - * Get the path to the configuration file based on the environment. - * @returns The path to the config.ini file. - */ -function getConfigFilePath() { - if (process.env.NODE_ENV === "production") { - // Path for production environment, assumes config.ini is in the dist folder - return path.resolve(__dirname, "..", "config.ini"); - } else { - // Path for local development environment - return path.resolve(__dirname, "..", "..", "config.ini"); - } -} - -/** - * Initialize the configuration by reading the config.ini file. - */ export function initConfig() { - const configFilePath = getConfigFilePath(); // Get the path to the config.ini file + const configFilePath = path.resolve(__dirname, "..", "..", "config.ini"); try { - const configContent = fs.readFileSync(configFilePath, "utf-8"); // Read the file content - config = ini.parse(configContent); // Parse the .ini file content into a JavaScript object + const configContent = fs.readFileSync(configFilePath, "utf-8"); + config = parseConfigContent(configContent); // Use a custom parsing function } catch (error) { console.error( `Error: The file '${configFilePath}' was not found or could not be read.` - ); // Log error if file cannot be read + ); + } +} + +function parseConfigContent(content: string): Config { + const lines = content.split("\n"); + const result: Config = {}; + + let currentSection: keyof Config | null = null; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine || trimmedLine.startsWith(";")) continue; // Skip empty or comment lines + + if (trimmedLine.startsWith("[") && trimmedLine.endsWith("]")) { + // New section + currentSection = trimmedLine.slice(1, -1) as keyof Config; + result[currentSection] = {}; // Initialize the section as an object + } else if (currentSection) { + const [key, value] = trimmedLine.split("=").map((part) => part.trim()); + + // Using type assertion to tell TypeScript that currentSection is valid + (result[currentSection] as Record)[key] = + value; + } } + + return result; } -/** - * Get the package name from the DEFAULT section of the config. - * @returns The package name or undefined if not set. - * @throws An error if the configuration is not initialized. - */ +// Function to get the package name from the DEFAULT section export function getPackageName(): string | undefined { if (config && config.DEFAULT) { - return config.DEFAULT.package_name as string; // Return package name + return config.DEFAULT.package_name as string; } else { - throw new Error("Configuration not initialized. Call initConfig() first."); // Error if config not initialized + throw new Error("Configuration not initialized. Call initConfig() first."); } } -/** - * Get the active secret status from the SECRET section of the config. - * @returns The active secret status or undefined if not set. - * @throws An error if the configuration is not initialized. - */ +// Function to get the active secret status from the SECRET section export function getActiveSecretStatus(): string | undefined { if (config && config.SECRET) { - return config.SECRET.secret_active as string; // Return active secret status + return config.SECRET.secret_active as string; } else { - throw new Error("Configuration not initialized. Call initConfig() first."); // Error if config not initialized + throw new Error("Configuration not initialized. Call initConfig() first."); } } -/** - * Get the inactive secret status from the SECRET section of the config. - * @returns The inactive secret status or undefined if not set. - * @throws An error if the configuration is not initialized. - */ +// Function to get the inactive secret status from the SECRET section export function getInactiveSecretStatus(): string | undefined { if (config && config.SECRET) { - return config.SECRET.secret_inactive as string; // Return inactive secret status + return config.SECRET.secret_inactive as string; } else { - throw new Error("Configuration not initialized. Call initConfig() first."); // Error if config not initialized + throw new Error("Configuration not initialized. Call initConfig() first."); } } -/** - * Get the version from the DEFAULT section of the config. - * @returns The version or undefined if not set. - * @throws An error if the configuration is not initialized. - */ +// Function to get the version from the DEFAULT section export function getVersion(): string | undefined { if (config && config.DEFAULT) { - return config.DEFAULT.version as string; // Return version + return config.DEFAULT.version as string; } else { - throw new Error("Configuration not initialized. Call initConfig() first."); // Error if config not initialized + throw new Error("Configuration not initialized. Call initConfig() first."); } } -// Automatically call initConfig when this module is imported to ensure configuration is loaded +// Call initConfig when this file is imported or used initConfig(); diff --git a/src/js/jsr.json b/src/js/jsr.json index e88eccf..16e93d8 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.2-beta.4", + "version": "0.0.2-beta.5", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { diff --git a/src/js/package.json b/src/js/package.json index fd1a1ae..124da60 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,6 +1,6 @@ { "name": "how2validate", - "version": "0.0.2-beta.3", + "version": "0.0.1-beta.1", "description": "A CLI tool to validate secrets for different services.", "main": "how2validate/index.ts", "scripts": { @@ -26,7 +26,6 @@ "dependencies": { "axios": "^1.7.7", "commander": "^12.1.0", - "ini": "^5.0.0", "jsr": "^0.13.2", "loglevel": "^1.9.2" }, @@ -38,12 +37,13 @@ "@semantic-release/release-notes-generator": "^14.0.1", "@types/ini": "^4.1.1", "@types/node": "^22.5.5", - "cpx": "^1.5.0", + "cpx": "^1.2.1", "semantic-release": "^24.1.1", "typescript": "^5.6.2" }, "files": [ - "config.ini", - "tokenManager.json" + "how2validate/*", + "./config.ini", + "./tokenManager.json" ] } diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index 2c30a1b..781eb4d 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -1,13 +1,17 @@ { "compilerOptions": { - "target": "ES6", // Specifies the JavaScript version to compile to. ES6 (also known as ES2015) is a widely supported version. - "module": "CommonJS", // Defines the module system to use. CommonJS is the standard for Node.js modules. - "outDir": "./dist", // The output directory where compiled JavaScript files will be placed. - "rootDir": "./how2validate", // The root directory of your TypeScript source files; all files under this directory will be included in the compilation. - "strict": true, // Enables all strict type-checking options for more rigorous checks. - "esModuleInterop": true, // Allows default imports from modules with no default export. Enables interoperability between CommonJS and ES modules. - "skipLibCheck": true, // Skips type checking of declaration files (e.g., `.d.ts` files), speeding up the compilation process. + "target": "ES6", // Specifies the JavaScript version to compile to. ES6 (also known as ES2015) is a widely supported version. + "module": "CommonJS", // Defines the module system to use. CommonJS is the standard for Node.js modules. + "outDir": "./dist", // The output directory where compiled JavaScript files will be placed. + "rootDir": "./how2validate", // The root directory of your TypeScript source files; all files under this directory will be included in the compilation. + "strict": true, // Enables all strict type-checking options for more rigorous checks. + "esModuleInterop": true, // Allows default imports from modules with no default export. Enables interoperability between CommonJS and ES modules. + "skipLibCheck": true, // Skips type checking of declaration files (e.g., `.d.ts` files), speeding up the compilation process. "forceConsistentCasingInFileNames": true // Ensures that file name casing is consistent across the project to prevent errors on case-sensitive file systems. }, - "include": ["how2validate/**/*"] // Specifies which files to include in the project; here, it includes all files in the `how2validate` directory and its subdirectories. + "include": [ + "how2validate/**/*", + "./config.ini", // Include the config.ini file from source + "./tokenManager.json" // Include the tokenManager.json file from source + ] } From cdf5de348275c6e7f7dcfaa1ed40f5dedb571f4d Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Fri, 27 Sep 2024 16:52:38 +0530 Subject: [PATCH 017/106] docs(cli):updated cli help summary --- ...cker_publish.yml => container_publish.yml} | 0 src/python/config.ini | 2 +- .../how2validate/utility/tool_utility.py | 39 +++++++++++++++++++ src/python/how2validate/validator.py | 25 ++++++++---- src/python/requirements.in | 3 +- src/python/requirements.txt | 2 + 6 files changed, 61 insertions(+), 10 deletions(-) rename .github/workflows/{docker_publish.yml => container_publish.yml} (100%) diff --git a/.github/workflows/docker_publish.yml b/.github/workflows/container_publish.yml similarity index 100% rename from .github/workflows/docker_publish.yml rename to .github/workflows/container_publish.yml diff --git a/src/python/config.ini b/src/python/config.ini index 4857e2f..fda8da5 100644 --- a/src/python/config.ini +++ b/src/python/config.ini @@ -1,6 +1,6 @@ [DEFAULT] package_name = how2validate -version = 0.0.0a8 +version = 0.0.1-beta.0 [SECRET] secret_active = Active diff --git a/src/python/how2validate/utility/tool_utility.py b/src/python/how2validate/utility/tool_utility.py index cbecfb7..3578ffe 100644 --- a/src/python/how2validate/utility/tool_utility.py +++ b/src/python/how2validate/utility/tool_utility.py @@ -5,6 +5,8 @@ import subprocess import sys +from tabulate import tabulate + from how2validate.utility.config_utility import get_package_name from how2validate.utility.log_utility import setup_logging @@ -43,6 +45,43 @@ def get_secretservices(file_path = file_path): return enabled_secrets_services +def get_secretscope(file_path = file_path): + """ + Reads a JSON file containing provider tokens and logs a table of enabled services. + + Args: + file_path (str): The path to the JSON file containing provider tokens. + Defaults to the global variable `file_path`. + + Raises: + FileNotFoundError: If the specified file cannot be found or read. + + Returns: + None: This function logs a formatted table of enabled provider services. + If no enabled services are found, it logs a relevant message. + """ + # Open and load the JSON file + with open(file_path, 'r') as f: + data = json.load(f) + + scoped_services = [] + + # Iterate over each provider and its tokens + for provider, tokens in data.items(): + for token_info in tokens: + # Check if the token is enabled + if token_info['is_enabled']: + # Add the provider and service (token display name) to the scoped services list + scoped_services.append([provider, token_info['display_name']]) + + # Log the result as a table + if scoped_services: + # Log the table using the tabulate function with fancy formatting + logging.info(tabulate(scoped_services, headers=['Provider', 'Service'], tablefmt='fancy_outline')) + else: + # Log a message if no enabled services are found + logging.info("No enabled services found.") + def format_serviceprovider(file_path = file_path): """Format service choices as a bullet-point list.""" with open(file_path, 'r') as f: diff --git a/src/python/how2validate/validator.py b/src/python/how2validate/validator.py index c684a48..ffa355e 100644 --- a/src/python/how2validate/validator.py +++ b/src/python/how2validate/validator.py @@ -1,8 +1,8 @@ import argparse import logging -from how2validate.utility.config_utility import get_active_secret_status, get_inactive_secret_status, get_package_name, get_version -from how2validate.utility.tool_utility import format_serviceprovider, format_services, format_string, get_secretprovider, get_secretservices, redact_secret, update_tool, validate_choice +from how2validate.utility.config_utility import get_active_secret_status, get_inactive_secret_status, get_version +from how2validate.utility.tool_utility import format_string, get_secretprovider, get_secretscope, get_secretservices, redact_secret, update_tool, validate_choice from how2validate.utility.log_utility import setup_logging from how2validate.handler.validator_handler import validator_handle_service @@ -30,18 +30,20 @@ def parse_arguments(): services = get_secretservices() # Define arguments + parser.add_argument('-secretscope', action='store_true', + help='Explore the secret universe. Your next target awaits.') parser.add_argument('-provider', type=lambda s: validate_choice(s, provider), required=False, - help=f"Secret provider to validate secrets\nSupported providers:\n{format_serviceprovider()}") + help=f"Specify your provider. Unleash your validation arsenal.") parser.add_argument('-service', type=lambda s: validate_choice(s, services), required=False, - help=f"Service / SecretType to validate secrets\nSupported services:\n{format_services()}") + help=f"Specify your target service. Validate your secrets with precision.") parser.add_argument('-secret', required=False, - help="Pass Secrets to be validated") + help="Unveil your secrets to verify their authenticity.") parser.add_argument('-r', '--response', action='store_true', - help=f"Prints {get_active_secret_status()}/ {get_inactive_secret_status()} upon validating secrets.") + help=f"Monitor the status. View if your secret {get_active_secret_status()} or {get_inactive_secret_status()}.") parser.add_argument('-report', action='store_false', default=False, - help=f"Reports validated secrets over E-mail") + help=f"Get detailed reports. Receive validated secrets via email [Alpha Feature].") parser.add_argument('-v', '--version', action='version', version=f'How2Validate Tool version {get_version()}', - help='Expose the version') + help='Expose the version.') parser.add_argument('--update', action='store_true', help='Hack the tool to the latest version.') @@ -66,6 +68,13 @@ def main(args=None): except Exception as e: logging.error(f"Error during tool update: {e}") return + + if args.secretscope: + try: + get_secretscope() + except Exception as e: + logging.error(f"Error fetching Scoped secret services : {e}") + return if not args.provider or not args.service or not args.secret: logging.error("Missing required arguments: -Provider, -Service, -Secret") diff --git a/src/python/requirements.in b/src/python/requirements.in index b3e275d..205f638 100644 --- a/src/python/requirements.in +++ b/src/python/requirements.in @@ -11,4 +11,5 @@ twine pyOpenSSL wheel pymongo -python-dotenv \ No newline at end of file +python-dotenv +tabulate \ No newline at end of file diff --git a/src/python/requirements.txt b/src/python/requirements.txt index ac568bf..3cc200d 100644 --- a/src/python/requirements.txt +++ b/src/python/requirements.txt @@ -65,6 +65,8 @@ rfc3986==2.0.0 # via twine rich==13.8.1 # via twine +tabulate==0.9.0 + # via -r ./requirements.in twine==5.1.1 # via -r ./requirements.in urllib3==2.2.3 From e4e1f93861d0d9fcfe79d3dd47a69031212dcf20 Mon Sep 17 00:00:00 2001 From: VigneshKanna <35601923+Vigneshkna@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:01:53 +0530 Subject: [PATCH 018/106] Fix code scanning alert no. 1: Clear-text logging of sensitive information Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/python/how2validate/validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/how2validate/validator.py b/src/python/how2validate/validator.py index ffa355e..9d67ef7 100644 --- a/src/python/how2validate/validator.py +++ b/src/python/how2validate/validator.py @@ -82,7 +82,7 @@ def main(args=None): return try: - logging.info(f"Initiating validation for service: {args.service} with secret: {redact_secret(args.secret)}") + logging.info(f"Initiating validation for service: {args.service} with a provided secret.") result = validate(args.provider, args.service, args.secret, args.response, args.report) logging.info("Validation completed successfully.") except Exception as e: From 995dec080baae039ea1ec1c7526ccce4810b481e Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Fri, 27 Sep 2024 20:09:37 +0530 Subject: [PATCH 019/106] docs(cli):updated js cli help --- src/js/config.ini | 2 +- src/js/how2validate/index.ts | 30 +++++++++++++----- src/js/how2validate/utility/tool_utility.ts | 34 +++++++++++++++++++++ src/js/jsr.json | 2 +- src/js/package.json | 7 +++-- 5 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/js/config.ini b/src/js/config.ini index 0bcf49c..d54e0d4 100644 --- a/src/js/config.ini +++ b/src/js/config.ini @@ -1,6 +1,6 @@ [DEFAULT] package_name = @how2validate/how2validate -version = 0.0.1 +version = 0.0.1-beta.3 [SECRET] secret_active = Active diff --git a/src/js/how2validate/index.ts b/src/js/how2validate/index.ts index c71bee8..f8633c7 100644 --- a/src/js/how2validate/index.ts +++ b/src/js/how2validate/index.ts @@ -10,6 +10,7 @@ import { formatServiceProviders, formatServices, getSecretProviders, + getSecretscope, getSecretServices, redactSecret, updateTool, @@ -29,7 +30,7 @@ program .version( `How2Validate Tool version ${getVersion() as string}`, "-v, --version", - "Expose the version" + "Expose the version." ); // Set the version and help flag const providerChoices = getSecretProviders(); // Get supported secret providers @@ -37,23 +38,27 @@ const serviceChoices = getSecretServices(); // Get supported secret services // Define CLI options using Commander program + .option( + "-secretscope", + `Explore the secret universe. Your next target awaits.` + ) // Option for secret scope .option( "-provider ", - `Secret provider to validate secrets\nSupported providers:\n${formatServiceProviders()}`, + `Specify your provider. Unleash your validation arsenal.`, (value) => validateChoice(value, providerChoices) ) // Option for provider .option( "-service ", - `Service / SecretType to validate secrets\nSupported services:\n${formatServices()}`, + `Specify your target service. Validate your secrets with precision.`, (value) => validateChoice(value, serviceChoices) ) // Option for service - .option("-secret ", "Pass the secret to be validated") // Option for secret + .option("-secret ", "Unveil your secrets to verify their authenticity.") // Option for secret .option( "-r, --response", - `Prints ${getActiveSecretStatus()} / ${getInactiveSecretStatus()} upon validating secrets` + `Monitor the status. View if your secret ${getActiveSecretStatus()} or ${getInactiveSecretStatus()}.` ) // Option for response - .option("-report", "Reports validated secrets over E-mail", false) // Option to report secrets via email - .option("--update", "Hack the tool to the latest version"); // Option to update the tool + .option("-report", "Get detailed reports. Receive validated secrets via email [Alpha Feature].", false) // Option to report secrets via email + .option("--update", "Hack the tool to the latest version."); // Option to update the tool // Function to validate the secret using the specified provider and service export async function validate( @@ -84,6 +89,17 @@ async function main() { value, ]) // Normalize option keys ); + + // Check for the secretscope option + if (options.secretscope) { + try { + getSecretscope(); + return; // Exit after updating + } catch (error) { + console.error(`Error fetching Scoped secret services : ${error}`); // Log any errors + return; + } + } // Check for the update option if (options.update) { diff --git a/src/js/how2validate/utility/tool_utility.ts b/src/js/how2validate/utility/tool_utility.ts index bf80420..415d924 100644 --- a/src/js/how2validate/utility/tool_utility.ts +++ b/src/js/how2validate/utility/tool_utility.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; // Importing the 'fs' module for file system operations import * as path from "path"; // Importing the 'path' module for handling file and directory paths import * as logging from "loglevel"; // Importing loglevel for logging messages +import Table from 'cli-table3'; import { execSync } from "child_process"; // Importing execSync to run shell commands synchronously import { getPackageName } from "./config_utility"; // Importing a function to get the package name from configuration @@ -68,6 +69,39 @@ export function getSecretServices( return enabledSecretsServices; // Return the list of enabled secret services } +// Function to get and display secret providers and services +export function getSecretscope(filePath: string = tokenManager_filePath): void { + // Read and parse the JSON file contents + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + const table = new Table({ + head: ['Provider', 'Service'], // Table headers + style: { + head: ['green'], // Header styling + border: ['white'] // Border styling + } + }); + + let hasEnabledServices = false; + + // Iterate over each provider and their tokens + for (const provider in data) { + const tokens = data[provider]; + for (const tokenInfo of tokens) { + if (tokenInfo.is_enabled) { + table.push([provider, tokenInfo['display_name']]); // Add rows to the table + hasEnabledServices = true; + } + } + } + + // Display the table or a message if no services are enabled + if (hasEnabledServices) { + console.log(table.toString()); + } else { + console.log('No enabled services found.'); + } +} + /** * Format the enabled service providers for display. * @param filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). diff --git a/src/js/jsr.json b/src/js/jsr.json index 16e93d8..dcd31f7 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.2-beta.5", + "version": "0.0.2-beta.6", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { diff --git a/src/js/package.json b/src/js/package.json index 124da60..4805953 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,11 +1,13 @@ { "name": "how2validate", - "version": "0.0.1-beta.1", + "version": "0.0.1-beta.3", "description": "A CLI tool to validate secrets for different services.", "main": "how2validate/index.ts", "scripts": { "build": "npx tsc && npx cpx './config.ini' dist/ && npx cpx './tokenManager.json' dist/", - "start": "node dist/index.js" + "start": "node dist/index.js", + "publishnpm": "npm publish", + "publishjsr": "npx jsr publish" }, "repository": { "type": "git", @@ -25,6 +27,7 @@ "homepage": "https://github.com/Blackplums/how2validate#readme", "dependencies": { "axios": "^1.7.7", + "cli-table3": "^0.6.5", "commander": "^12.1.0", "jsr": "^0.13.2", "loglevel": "^1.9.2" From c299897b31ace65cc6b69cfd90578924501448f7 Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Fri, 27 Sep 2024 21:05:46 +0530 Subject: [PATCH 020/106] doc(JSdoc): updated module comments --- src/js/config.ini | 2 +- src/js/how2validate/index.ts | 56 +++++++++++++------ src/js/how2validate/utility/config_utility.ts | 26 +++++---- src/js/how2validate/utility/log_utility.ts | 12 ++-- .../validators/npm/npm_access_token.ts | 32 ++++++----- .../validators/snyk/snyk_auth_key.ts | 34 ++++++----- .../validators/sonarcloud/sonarcloud_token.ts | 32 ++++++----- src/js/jsr.json | 2 +- src/js/package.json | 2 +- 9 files changed, 119 insertions(+), 79 deletions(-) diff --git a/src/js/config.ini b/src/js/config.ini index d54e0d4..95cb03c 100644 --- a/src/js/config.ini +++ b/src/js/config.ini @@ -1,6 +1,6 @@ [DEFAULT] package_name = @how2validate/how2validate -version = 0.0.1-beta.3 +version = 0.0.1-beta.4 [SECRET] secret_active = Active diff --git a/src/js/how2validate/index.ts b/src/js/how2validate/index.ts index f8633c7..ac061c6 100644 --- a/src/js/how2validate/index.ts +++ b/src/js/how2validate/index.ts @@ -3,16 +3,12 @@ import { setupLogging } from "./utility/log_utility"; // Importing the logging s import { getActiveSecretStatus, getInactiveSecretStatus, - getPackageName, getVersion, } from "./utility/config_utility"; // Importing configuration utility functions import { - formatServiceProviders, - formatServices, getSecretProviders, getSecretscope, getSecretServices, - redactSecret, updateTool, validateChoice, } from "./utility/tool_utility"; // Importing utility functions for secret validation @@ -36,38 +32,57 @@ program const providerChoices = getSecretProviders(); // Get supported secret providers const serviceChoices = getSecretServices(); // Get supported secret services -// Define CLI options using Commander +/** + * Define CLI options using Commander. + * - secretscope: Option for secret scope + * - provider: Option to specify a provider with validation + * - service: Option to specify a service with validation + * - secret: Option to specify the secret to validate + * - response: Option to check if the secret is active or inactive + * - report: Option to get reports via email (Alpha feature) + * - update: Option to update the tool + */ program .option( "-secretscope", `Explore the secret universe. Your next target awaits.` - ) // Option for secret scope + ) .option( "-provider ", `Specify your provider. Unleash your validation arsenal.`, (value) => validateChoice(value, providerChoices) - ) // Option for provider + ) .option( "-service ", `Specify your target service. Validate your secrets with precision.`, (value) => validateChoice(value, serviceChoices) - ) // Option for service - .option("-secret ", "Unveil your secrets to verify their authenticity.") // Option for secret + ) + .option("-secret ", "Unveil your secrets to verify their authenticity.") .option( "-r, --response", `Monitor the status. View if your secret ${getActiveSecretStatus()} or ${getInactiveSecretStatus()}.` - ) // Option for response - .option("-report", "Get detailed reports. Receive validated secrets via email [Alpha Feature].", false) // Option to report secrets via email - .option("--update", "Hack the tool to the latest version."); // Option to update the tool + ) + .option("-report", "Get detailed reports. Receive validated secrets via email [Alpha Feature].", false) + .option("--update", "Hack the tool to the latest version."); -// Function to validate the secret using the specified provider and service +/** + * Validate the provided secret using the given provider, service, and options. + * + * @param {string} provider - The provider to use for validation. + * @param {string} service - The service to validate the secret with. + * @param {string} secret - The secret that needs to be validated. + * @param {boolean} response - Whether to get a response status for the secret. + * @param {boolean} report - Whether to generate a report for the validation. + * + * @returns {Promise} - A promise that resolves when validation is complete. + */ export async function validate( provider: string, service: string, secret: string, response: boolean, report: boolean -) { +): Promise { console.info("Started validating secret..."); const result = await validatorHandleService( service, @@ -78,8 +93,13 @@ export async function validate( console.info(result); } -// Main function to execute the CLI program logic -async function main() { +/** + * Main function that executes the CLI program logic. + * Parses the command-line arguments and performs actions based on the options provided. + * + * @returns {Promise} - A promise that resolves when the program execution is complete. + */ +async function main(): Promise { program.parse(process.argv); // Parse command-line arguments // Convert options keys to lowercase for consistency @@ -89,7 +109,7 @@ async function main() { value, ]) // Normalize option keys ); - + // Check for the secretscope option if (options.secretscope) { try { @@ -126,7 +146,7 @@ async function main() { console.info( `Initiating validation for service: ${ options.service - } with secret: ${redactSecret(options.secret)}` + } with a provided secret.` ); await validate( options.provider, diff --git a/src/js/how2validate/utility/config_utility.ts b/src/js/how2validate/utility/config_utility.ts index a319ebb..6a98051 100644 --- a/src/js/how2validate/utility/config_utility.ts +++ b/src/js/how2validate/utility/config_utility.ts @@ -14,19 +14,29 @@ interface Config { let config: Config | null = null; -export function initConfig() { - const configFilePath = path.resolve(__dirname, "..", "..", "config.ini"); +/** + * Initialize the configuration by reading the config.ini file. + * Optionally accepts a custom file path. + * @param {string} [configFilePath] - The custom file path for config.ini (optional). + */ +export function initConfig(configFilePath?: string) { + const resolvedPath = configFilePath || path.resolve(__dirname, "..", "..", "config.ini"); try { - const configContent = fs.readFileSync(configFilePath, "utf-8"); + const configContent = fs.readFileSync(resolvedPath, "utf-8"); config = parseConfigContent(configContent); // Use a custom parsing function } catch (error) { console.error( - `Error: The file '${configFilePath}' was not found or could not be read.` + `Error: The file '${resolvedPath}' was not found or could not be read.` ); } } +/** + * Parse the content of the config.ini file and convert it to a Config object. + * @param {string} content - The content of the config.ini file. + * @returns {Config} - The parsed configuration object. + */ function parseConfigContent(content: string): Config { const lines = content.split("\n"); const result: Config = {}; @@ -45,8 +55,7 @@ function parseConfigContent(content: string): Config { const [key, value] = trimmedLine.split("=").map((part) => part.trim()); // Using type assertion to tell TypeScript that currentSection is valid - (result[currentSection] as Record)[key] = - value; + (result[currentSection] as Record)[key] = value; } } @@ -62,7 +71,6 @@ export function getPackageName(): string | undefined { } } -// Function to get the active secret status from the SECRET section export function getActiveSecretStatus(): string | undefined { if (config && config.SECRET) { return config.SECRET.secret_active as string; @@ -71,7 +79,6 @@ export function getActiveSecretStatus(): string | undefined { } } -// Function to get the inactive secret status from the SECRET section export function getInactiveSecretStatus(): string | undefined { if (config && config.SECRET) { return config.SECRET.secret_inactive as string; @@ -80,7 +87,6 @@ export function getInactiveSecretStatus(): string | undefined { } } -// Function to get the version from the DEFAULT section export function getVersion(): string | undefined { if (config && config.DEFAULT) { return config.DEFAULT.version as string; @@ -89,5 +95,5 @@ export function getVersion(): string | undefined { } } -// Call initConfig when this file is imported or used +// Automatically initialize config when the file is imported initConfig(); diff --git a/src/js/how2validate/utility/log_utility.ts b/src/js/how2validate/utility/log_utility.ts index 8f7d55a..4009d4c 100644 --- a/src/js/how2validate/utility/log_utility.ts +++ b/src/js/how2validate/utility/log_utility.ts @@ -7,16 +7,16 @@ import { /** * Set up the logging configuration. * This function initializes the logging level to INFO to control the verbosity of log messages. + * @param {logging.LogLevelDesc} [level='INFO'] - Optional parameter to set the logging level. */ -export function setupLogging() { - logging.setLevel("INFO"); // Set logging level to INFO +export function setupLogging(level: logging.LogLevelDesc = "INFO") { + logging.setLevel(level); // Set logging level, default to INFO } /** * Generate a message about the status of a secret. * @param service - The name of the service associated with the secret. * @param isActive - The current status of the secret (active or inactive). - * @param response - Optional parameter to include additional response data. * @param responseData - Optional data to provide more context if a response exists. * @returns A formatted message describing the secret's status. * @throws An error if the isActive value is not recognized. @@ -27,7 +27,6 @@ export function getSecretStatusMessage( response?: boolean, responseData?: any ): string { - // Normalize isActive values to handle both 'Active' and 'InActive' let status: string; // Check if the secret is active or inactive based on provided status @@ -36,7 +35,6 @@ export function getSecretStatusMessage( } else if (isActive === getInactiveSecretStatus()) { status = "inactive and not operational"; // Set status message for inactive secret } else { - // Throw an error for unexpected isActive values throw new Error( `Unexpected isActive value: ${isActive}. Expected 'Active' or 'InActive'.` ); @@ -45,8 +43,8 @@ export function getSecretStatusMessage( // Base message about the secret's status let message = `The provided secret '${service}' is currently ${status}.`; - // If a response exists, append it to the message - if (response) { + // If response data exists, append it to the message + if (responseData) { message += ` Here is the additional response data:\n${responseData}`; } diff --git a/src/js/how2validate/validators/npm/npm_access_token.ts b/src/js/how2validate/validators/npm/npm_access_token.ts index ea56c23..f47cd52 100644 --- a/src/js/how2validate/validators/npm/npm_access_token.ts +++ b/src/js/how2validate/validators/npm/npm_access_token.ts @@ -7,11 +7,13 @@ import { getSecretStatusMessage } from "../../utility/log_utility"; // Import fu /** * Validate an NPM access token by making an API request to check its validity. - * @param service - The name of the service being validated (in this case, NPM). - * @param secret - The NPM access token to validate. - * @param response - A flag to indicate whether to include detailed response data. - * @param report - An optional flag for additional reporting features. - * @returns A promise that resolves to a status message about the access token validation. + * + * @param {string} service - The name of the service being validated (in this case, NPM). + * @param {string} secret - The NPM access token to validate. + * @param {boolean} response - A flag to indicate whether to include detailed response data. + * @param {boolean} [report] - An optional flag for additional reporting features. + * + * @returns {Promise} - A promise that resolves to a status message about the access token validation. */ export async function validateNpmAccessToken( service: string, @@ -62,10 +64,12 @@ export async function validateNpmAccessToken( /** * Handle the case when the NPM access token is inactive or invalid. - * @param service - The name of the service being validated (NPM). - * @param response - A flag to indicate whether to include detailed response data. - * @param data - Optional additional data to include in the response. - * @returns A formatted status message indicating the inactive status. + * + * @param {string} service - The name of the service being validated (NPM). + * @param {boolean} response - A flag to indicate whether to include detailed response data. + * @param {any} [data] - Optional additional data to include in the response. + * + * @returns {string} - A formatted status message indicating the inactive status. */ function handleInactiveStatus( service: string, @@ -92,10 +96,12 @@ function handleInactiveStatus( /** * Handle errors that occur during the validation process. - * @param error - The error object that was thrown during the request. - * @param service - The name of the service being validated (NPM). - * @param response - A flag to indicate whether to include detailed response data. - * @returns A formatted status message based on the type of error encountered. + * + * @param {unknown} error - The error object that was thrown during the request. + * @param {string} service - The name of the service being validated (NPM). + * @param {boolean} response - A flag to indicate whether to include detailed response data. + * + * @returns {string} - A formatted status message based on the type of error encountered. */ function handleErrors( error: unknown, diff --git a/src/js/how2validate/validators/snyk/snyk_auth_key.ts b/src/js/how2validate/validators/snyk/snyk_auth_key.ts index fabfa87..4acdac1 100644 --- a/src/js/how2validate/validators/snyk/snyk_auth_key.ts +++ b/src/js/how2validate/validators/snyk/snyk_auth_key.ts @@ -1,5 +1,3 @@ -import * as fs from "fs"; // Import the file system module for file operations -import * as path from "path"; // Import the path module for handling file paths import axios from "axios"; // Import Axios for making HTTP requests import { getActiveSecretStatus, @@ -9,11 +7,13 @@ import { getSecretStatusMessage } from "../../utility/log_utility"; // Import fu /** * Validate a Snyk authentication key by making an API request to check its validity. - * @param service - The name of the service being validated. - * @param secret - The Snyk API key (token) to validate. - * @param response - A flag to indicate whether to include detailed response data. - * @param report - An optional flag for additional reporting features. - * @returns A promise that resolves to a status message about the authentication key validation. + * + * @param {string} service - The name of the service being validated. + * @param {string} secret - The Snyk API key (token) to validate. + * @param {boolean} response - A flag to indicate whether to include detailed response data. + * @param {boolean} [report] - An optional flag for additional reporting features. + * + * @returns {Promise} - A promise that resolves to a status message about the authentication key validation. */ export async function validateSnykAuthKey( service: string, @@ -64,10 +64,12 @@ export async function validateSnykAuthKey( /** * Handle the case when the authentication key is inactive or invalid. - * @param service - The name of the service being validated. - * @param response - A flag to indicate whether to include detailed response data. - * @param responseData - Optional additional data to include in the response. - * @returns A formatted status message indicating the inactive status. + * + * @param {string} service - The name of the service being validated. + * @param {boolean} response - A flag to indicate whether to include detailed response data. + * @param {any} [responseData] - Optional additional data to include in the response. + * + * @returns {string} - A formatted status message indicating the inactive status. */ function handleInactiveResponse( service: string, @@ -94,10 +96,12 @@ function handleInactiveResponse( /** * Handle errors that occur during the validation process. - * @param service - The name of the service being validated. - * @param response - A flag to indicate whether to include detailed response data. - * @param error - The error object that was thrown. - * @returns A formatted status message based on the type of error encountered. + * + * @param {string} service - The name of the service being validated. + * @param {boolean} response - A flag to indicate whether to include detailed response data. + * @param {any} error - The error object that was thrown. + * + * @returns {string} - A formatted status message based on the type of error encountered. */ function handleError(service: string, response: boolean, error: any): string { let errorMessage = ""; diff --git a/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts b/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts index 15a9b84..83792ae 100644 --- a/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts +++ b/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts @@ -7,11 +7,13 @@ import { getSecretStatusMessage } from "../../utility/log_utility"; // Import fu /** * Validate a SonarCloud token by making an API request to check its validity. - * @param service - The name of the service being validated. - * @param secret - The token or secret to validate. - * @param response - A flag to indicate whether to include detailed response data. - * @param report - An optional flag for additional reporting features. - * @returns A promise that resolves to a status message about the token validation. + * + * @param {string} service - The name of the service being validated. + * @param {string} secret - The token or secret to validate. + * @param {boolean} response - A flag to indicate whether to include detailed response data. + * @param {boolean} [report] - An optional flag for additional reporting features. + * + * @returns {Promise} - A promise that resolves to a status message about the token validation. */ export async function validateSonarcloudToken( service: string, @@ -59,10 +61,12 @@ export async function validateSonarcloudToken( /** * Handle the case when the token is inactive or invalid. - * @param service - The name of the service being validated. - * @param response - A flag to indicate whether to include detailed response data. - * @param data - Optional additional data to include in the response. - * @returns A formatted status message indicating the inactive status. + * + * @param {string} service - The name of the service being validated. + * @param {boolean} response - A flag to indicate whether to include detailed response data. + * @param {any} [data] - Optional additional data to include in the response. + * + * @returns {string} - A formatted status message indicating the inactive status. */ function handleInactiveStatus( service: string, @@ -87,10 +91,12 @@ function handleInactiveStatus( /** * Handle errors that occur during the validation process. - * @param error - The error object that was thrown. - * @param service - The name of the service being validated. - * @param response - A flag to indicate whether to include detailed response data. - * @returns A formatted status message based on the type of error. + * + * @param {unknown} error - The error object that was thrown. + * @param {string} service - The name of the service being validated. + * @param {boolean} response - A flag to indicate whether to include detailed response data. + * + * @returns {string} - A formatted status message based on the type of error. */ function handleErrors( error: unknown, diff --git a/src/js/jsr.json b/src/js/jsr.json index dcd31f7..b248804 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.2-beta.6", + "version": "0.0.2-beta.7", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { diff --git a/src/js/package.json b/src/js/package.json index 4805953..ea52ef0 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,6 +1,6 @@ { "name": "how2validate", - "version": "0.0.1-beta.3", + "version": "0.0.1-beta.4", "description": "A CLI tool to validate secrets for different services.", "main": "how2validate/index.ts", "scripts": { From 7ac8e074089372957f7981f1fcda7a6e86ea542c Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Fri, 27 Sep 2024 23:32:44 +0530 Subject: [PATCH 021/106] fix(__dir):fixing to abosolute path --- src/js/config.ini | 2 +- src/js/how2validate/utility/config_utility.ts | 4 ++-- src/js/jsr.json | 2 +- src/js/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/js/config.ini b/src/js/config.ini index 95cb03c..a5aab47 100644 --- a/src/js/config.ini +++ b/src/js/config.ini @@ -1,6 +1,6 @@ [DEFAULT] package_name = @how2validate/how2validate -version = 0.0.1-beta.4 +version = 0.0.1-beta.5 [SECRET] secret_active = Active diff --git a/src/js/how2validate/utility/config_utility.ts b/src/js/how2validate/utility/config_utility.ts index 6a98051..7e72c8e 100644 --- a/src/js/how2validate/utility/config_utility.ts +++ b/src/js/how2validate/utility/config_utility.ts @@ -1,5 +1,5 @@ -import * as path from "path"; -import * as fs from "fs"; +const fs = require('fs'); +const path = require('path'); interface Config { DEFAULT?: { diff --git a/src/js/jsr.json b/src/js/jsr.json index b248804..9d02ffb 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.2-beta.7", + "version": "0.0.2-beta.8", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { diff --git a/src/js/package.json b/src/js/package.json index ea52ef0..306a015 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,6 +1,6 @@ { "name": "how2validate", - "version": "0.0.1-beta.4", + "version": "0.0.1-beta.5", "description": "A CLI tool to validate secrets for different services.", "main": "how2validate/index.ts", "scripts": { From f75f3c347d1d2e6023d4b02515dbc969e8fdd6cd Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Sat, 28 Sep 2024 14:35:52 +0530 Subject: [PATCH 022/106] docs(readme):updated python readme --- src/js/config.ini | 2 +- src/js/how2validate/index.ts | 18 ++--- src/js/how2validate/utility/config_utility.ts | 4 +- src/js/jsr.json | 2 +- src/js/package.json | 3 +- src/js/tsconfig.json | 7 +- src/python/README.md | 80 +++++++++++++++++-- src/python/config.ini | 2 +- 8 files changed, 94 insertions(+), 24 deletions(-) diff --git a/src/js/config.ini b/src/js/config.ini index a5aab47..ddb5cab 100644 --- a/src/js/config.ini +++ b/src/js/config.ini @@ -1,6 +1,6 @@ [DEFAULT] package_name = @how2validate/how2validate -version = 0.0.1-beta.5 +version = 0.0.1-beta.6 [SECRET] secret_active = Active diff --git a/src/js/how2validate/index.ts b/src/js/how2validate/index.ts index ac061c6..f0b7aaf 100644 --- a/src/js/how2validate/index.ts +++ b/src/js/how2validate/index.ts @@ -50,12 +50,12 @@ program .option( "-provider ", `Specify your provider. Unleash your validation arsenal.`, - (value) => validateChoice(value, providerChoices) + (value: string) => validateChoice(value, providerChoices) ) .option( "-service ", `Specify your target service. Validate your secrets with precision.`, - (value) => validateChoice(value, serviceChoices) + (value: string) => validateChoice(value, serviceChoices) ) .option("-secret ", "Unveil your secrets to verify their authenticity.") .option( @@ -67,13 +67,13 @@ program /** * Validate the provided secret using the given provider, service, and options. - * + * * @param {string} provider - The provider to use for validation. * @param {string} service - The service to validate the secret with. * @param {string} secret - The secret that needs to be validated. * @param {boolean} response - Whether to get a response status for the secret. * @param {boolean} report - Whether to generate a report for the validation. - * + * * @returns {Promise} - A promise that resolves when validation is complete. */ export async function validate( @@ -96,7 +96,7 @@ export async function validate( /** * Main function that executes the CLI program logic. * Parses the command-line arguments and performs actions based on the options provided. - * + * * @returns {Promise} - A promise that resolves when the program execution is complete. */ async function main(): Promise { @@ -116,7 +116,7 @@ async function main(): Promise { getSecretscope(); return; // Exit after updating } catch (error) { - console.error(`Error fetching Scoped secret services : ${error}`); // Log any errors + console.error(`Error fetching Scoped secret services: ${error}`); // Log any errors return; } } @@ -144,9 +144,7 @@ async function main(): Promise { // Attempt to validate the secret try { console.info( - `Initiating validation for service: ${ - options.service - } with a provided secret.` + `Initiating validation for service: ${options.service} with a provided secret.` ); await validate( options.provider, @@ -163,4 +161,4 @@ async function main(): Promise { } // Start the main function and handle any unexpected errors -main().catch((error) => console.error(`Unexpected error: ${error}`)); +main().catch((error) => console.error(`Unexpected error: ${error}`)); \ No newline at end of file diff --git a/src/js/how2validate/utility/config_utility.ts b/src/js/how2validate/utility/config_utility.ts index 7e72c8e..6a98051 100644 --- a/src/js/how2validate/utility/config_utility.ts +++ b/src/js/how2validate/utility/config_utility.ts @@ -1,5 +1,5 @@ -const fs = require('fs'); -const path = require('path'); +import * as path from "path"; +import * as fs from "fs"; interface Config { DEFAULT?: { diff --git a/src/js/jsr.json b/src/js/jsr.json index 9d02ffb..8d64520 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.2-beta.8", + "version": "0.0.2-beta.9", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { diff --git a/src/js/package.json b/src/js/package.json index 306a015..343702f 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,6 +1,6 @@ { "name": "how2validate", - "version": "0.0.1-beta.5", + "version": "0.0.1-beta.6", "description": "A CLI tool to validate secrets for different services.", "main": "how2validate/index.ts", "scripts": { @@ -25,6 +25,7 @@ "url": "https://github.com/Blackplums/how2validate/issues" }, "homepage": "https://github.com/Blackplums/how2validate#readme", + "type": "commonjs", "dependencies": { "axios": "^1.7.7", "cli-table3": "^0.6.5", diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index 781eb4d..d66f2b0 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -1,13 +1,14 @@ { "compilerOptions": { - "target": "ES6", // Specifies the JavaScript version to compile to. ES6 (also known as ES2015) is a widely supported version. - "module": "CommonJS", // Defines the module system to use. CommonJS is the standard for Node.js modules. + "target": "ES2021", // Specifies the JavaScript version to compile to. ES6 (also known as ES2015) is a widely supported version. + "module": "commonjs", // Defines the module system to use. CommonJS is the standard for Node.js modules. "outDir": "./dist", // The output directory where compiled JavaScript files will be placed. "rootDir": "./how2validate", // The root directory of your TypeScript source files; all files under this directory will be included in the compilation. "strict": true, // Enables all strict type-checking options for more rigorous checks. "esModuleInterop": true, // Allows default imports from modules with no default export. Enables interoperability between CommonJS and ES modules. "skipLibCheck": true, // Skips type checking of declaration files (e.g., `.d.ts` files), speeding up the compilation process. - "forceConsistentCasingInFileNames": true // Ensures that file name casing is consistent across the project to prevent errors on case-sensitive file systems. + "forceConsistentCasingInFileNames": true, // Ensures that file name casing is consistent across the project to prevent errors on case-sensitive file systems. + "moduleResolution": "node" }, "include": [ "how2validate/**/*", diff --git a/src/python/README.md b/src/python/README.md index 9b45b53..eb97b9c 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -1,14 +1,84 @@ # How2Validate -How2Validate is a package designed to validate secrets and sensitive information across multiple platforms. +**How2Validate** is a security-focused tool designed to validate sensitive secrets by querying official secret provider endpoints. It provides real-time feedback on the authenticity of the credentials, ensuring that the secrets are valid. + +## Why How2Validate? +The need for **How2Validate** arises from the growing concern of exposing sensitive information in various applications, repositories, and environments. Leaked API keys, invalid credentials, and misconfigured secrets can lead to significant security vulnerabilities. **How2Validate** helps mitigate these risks by verifying secrets directly with the official providers before they are used in any system. ## Features -- Validate API keys, passwords, and other sensitive information. -- Cross-platform support (Windows, Linux, macOS). -- Easy integration with existing applications. +- **Validate API keys, passwords, and sensitive information**: It interacts with official provider authentication endpoints to ensure the authenticity of the secrets. +- **Cross-platform support**: Packages available for JavaScript, Python, and Docker environments. +- **Easy to use**: Simplifies secret validation with straightforward commands and functions. +- **Real-time feedback**: Instantly know the status of your secrets — whether they are valid or not. + +## How It Works + +**How2Validate** utilizes the official authentication endpoints provided by different service providers (like NPM, GitHub, Snyk, etc.) to validate secrets such as API keys, tokens, and other sensitive data. By querying these trusted endpoints, **How2Validate** ensures that the secrets are correct and not expired or invalid. + +For every provider, **How2Validate** relies on well-maintained libraries and packages suggested by those providers to handle the authentication process. + +## Detailed CLI Help + +The **How2Validate** tool provides multiple command-line options for validating secrets with precision. -## Installation +To see all available commands, use: ```bash +how2validate --help + +usage: How2Validate Tool + +Validate various types of secrets for different services. + +options: + -h, --help show this help message and exit + -secretscope Explore the secret universe. Your next target awaits. + -provider PROVIDER Specify your provider. Unleash your validation arsenal. + -service SERVICE Specify your target service. Validate your secrets with precision. + -secret SECRET Unveil your secrets to verify their authenticity. + -r, --response Monitor the status. View if your secret Active or InActive. + -report Get detailed reports. Receive validated secrets via email [Alpha Feature]. + -v, --version Expose the version. + --update Hack the tool to the latest version. + +Ensuring the authenticity of your secrets. +``` + +## How to Utilize the Functions + +**How2Validate** can be easily installed and used programmatically within Python projects. + +### Install the package: + +```py pip install how2validate +``` + +### Import the package and use the validate function: + +```py +from how2validate import validate + +# Validate secrets programmatically +validation_result = validate(provider,service, secret, response, report) +print(validation_result) + +``` + +### Example usage of validate function: + +```py +from how2validate import validate + +# Validate secrets programmatically +validation_result = validate( + provider="NPM", + service="NPM Access Token", + secret="<>", + response=False, + report=False, +) +print(validation_result) + +``` \ No newline at end of file diff --git a/src/python/config.ini b/src/python/config.ini index fda8da5..8157a4a 100644 --- a/src/python/config.ini +++ b/src/python/config.ini @@ -1,6 +1,6 @@ [DEFAULT] package_name = how2validate -version = 0.0.1-beta.0 +version = 0.0.1-beta.1 [SECRET] secret_active = Active From 15465dfea97beb599bf9b50214f80232d580340a Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Sat, 28 Sep 2024 23:44:01 +0530 Subject: [PATCH 023/106] chore(js): js package updates --- src/js/README.md | 84 +++++++++++- src/js/config.ini | 2 +- .../how2validate/handler/validator_handler.ts | 29 ++-- src/js/how2validate/index.ts | 85 +++++++++--- src/js/how2validate/utility/config_utility.ts | 81 ++++++++++- src/js/how2validate/utility/log_utility.ts | 43 +++--- src/js/how2validate/utility/tool_utility.ts | 128 +++++++++--------- .../validators/npm/npm_access_token.ts | 32 +++-- .../validators/snyk/snyk_auth_key.ts | 121 +++++++++++------ .../validators/sonarcloud/sonarcloud_token.ts | 20 ++- src/js/jsr.json | 2 +- src/js/package.json | 13 +- src/js/tsconfig.json | 3 +- 13 files changed, 455 insertions(+), 188 deletions(-) mode change 100644 => 100755 src/js/how2validate/index.ts diff --git a/src/js/README.md b/src/js/README.md index 9eb8855..a0781d7 100644 --- a/src/js/README.md +++ b/src/js/README.md @@ -1,14 +1,86 @@ # How2Validate -How2Validate is a package designed to validate secrets and sensitive information across multiple platforms. +**How2Validate** is a security-focused tool designed to validate sensitive secrets by querying official secret provider endpoints. It provides real-time feedback on the authenticity of the credentials, ensuring that the secrets are valid. + +## Why How2Validate? +The need for **How2Validate** arises from the growing concern of exposing sensitive information in various applications, repositories, and environments. Leaked API keys, invalid credentials, and misconfigured secrets can lead to significant security vulnerabilities. **How2Validate** helps mitigate these risks by verifying secrets directly with the official providers before they are used in any system. ## Features -- Validate API keys, passwords, and other sensitive information. -- Cross-platform support (Windows, Linux, macOS). -- Easy integration with existing applications. +- **Validate API keys, passwords, and sensitive information**: It interacts with official provider authentication endpoints to ensure the authenticity of the secrets. +- **Cross-platform support**: Packages available for JavaScript, Python, and Docker environments. +- **Easy to use**: Simplifies secret validation with straightforward commands and functions. +- **Real-time feedback**: Instantly know the status of your secrets — whether they are valid or not. + +## How It Works + +**How2Validate** utilizes the official authentication endpoints provided by different service providers (like NPM, GitHub, Snyk, etc.) to validate secrets such as API keys, tokens, and other sensitive data. By querying these trusted endpoints, **How2Validate** ensures that the secrets are correct and not expired or invalid. + +For every provider, **How2Validate** relies on well-maintained libraries and packages suggested by those providers to handle the authentication process. + +## Detailed CLI Help + +The **How2Validate** tool provides multiple command-line options for validating secrets with precision. + +To see all available commands, use: + +```npm +how2validate --help + +usage: How2Validate Tool + +Validate various types of secrets for different services. + +options: + -h, --help show this help message and exit + -secretscope Explore the secret universe. Your next target awaits. + -provider PROVIDER Specify your provider. Unleash your validation arsenal. + -service SERVICE Specify your target service. Validate your secrets with precision. + -secret SECRET Unveil your secrets to verify their authenticity. + -r, --response Monitor the status. View if your secret Active or InActive. + -report Get detailed reports. Receive validated secrets via email [Alpha Feature]. + -v, --version Expose the version. + --update Hack the tool to the latest version. + +Ensuring the authenticity of your secrets. +``` -## Installation +## How to Utilize the Functions + +**How2Validate** can be easily installed and used programmatically within projects. + +### Install the package: + +- Npm ```bash -npx jsr add @how2validate/how2validate +npm install how2validate +``` + +#### Import the package and use the validate function: + +```js +import { validate } from 'how2validate' + +# Validate secrets programmatically +var validation_result = validate(provider,service, secret, response, report) +print(validation_result) + +``` + +### Example usage of validate function: + +```js +import { validate } from 'how2validate' + +# Validate secrets programmatically +var validation_result = validate( + provider="NPM", + service="NPM Access Token", + secret="<>", + response=False, + report=False, +) +print(validation_result) + +``` \ No newline at end of file diff --git a/src/js/config.ini b/src/js/config.ini index ddb5cab..3cec010 100644 --- a/src/js/config.ini +++ b/src/js/config.ini @@ -1,6 +1,6 @@ [DEFAULT] package_name = @how2validate/how2validate -version = 0.0.1-beta.6 +version = 0.0.1-beta.14 [SECRET] secret_active = Active diff --git a/src/js/how2validate/handler/validator_handler.ts b/src/js/how2validate/handler/validator_handler.ts index 4636169..bbe14df 100644 --- a/src/js/how2validate/handler/validator_handler.ts +++ b/src/js/how2validate/handler/validator_handler.ts @@ -1,14 +1,14 @@ -import { validateSnykAuthKey } from "../validators/snyk/snyk_auth_key"; // Import the Snyk authentication key validator -import { validateSonarcloudToken } from "../validators/sonarcloud/sonarcloud_token"; // Import the Sonarcloud token validator -import { validateNpmAccessToken } from "../validators/npm/npm_access_token"; // Import the NPM access token validator +import { validateSnykAuthKey } from "../validators/snyk/snyk_auth_key.js"; // Import the Snyk authentication key validator +import { validateSonarcloudToken } from "../validators/sonarcloud/sonarcloud_token.js"; // Import the Sonarcloud token validator +import { validateNpmAccessToken } from "../validators/npm/npm_access_token.js"; // Import the NPM access token validator // Define a type for the validator function signature type ValidatorFunction = ( - service: string, - secret: string, - response: boolean, - report?: boolean -) => Promise; + service: string, // The name of the service being validated + secret: string, // The secret (e.g., API key, token) to validate + response: boolean, // Indicates whether to include response data in the output + report?: boolean // Optional parameter for additional reporting functionality +) => Promise; // The function returns a promise that resolves to a validation result message // Map of service names to their corresponding validator functions const serviceHandlers: Record = { @@ -20,11 +20,14 @@ const serviceHandlers: Record = { /** * Handle the validation of a service's secret. - * @param service - The name of the service to validate. - * @param secret - The secret (e.g., API key, token) to validate. - * @param response - A boolean indicating whether to include response data in the output. - * @param report - An optional parameter for additional reporting functionality. - * @returns A promise that resolves to a string message indicating the validation result. + * This function retrieves the appropriate validator function for the specified service + * and invokes it with the provided secret and parameters. + * + * @param {string} service - The name of the service to validate. + * @param {string} secret - The secret (e.g., API key, token) to validate. + * @param {boolean} response - A boolean indicating whether to include response data in the output. + * @param {boolean} [report] - An optional parameter for additional reporting functionality. + * @returns {Promise} A promise that resolves to a string message indicating the validation result. */ export async function validatorHandleService( service: string, diff --git a/src/js/how2validate/index.ts b/src/js/how2validate/index.ts old mode 100644 new mode 100755 index f0b7aaf..2bd5999 --- a/src/js/how2validate/index.ts +++ b/src/js/how2validate/index.ts @@ -1,25 +1,40 @@ +/** + * @module How2Validate Tool + * @description + * A Command-Line Interface (CLI) tool designed to validate various types of secrets across different services. + * It leverages multiple secret providers and services to ensure the authenticity and status of secrets. + * + * @requires commander + * @requires ./utility/config_utility.js + * @requires ./utility/tool_utility.js + * @requires ./handler/validator_handler.js + */ + import { Command } from "commander"; // Importing Commander for building CLI applications -import { setupLogging } from "./utility/log_utility"; // Importing the logging setup function import { getActiveSecretStatus, getInactiveSecretStatus, getVersion, -} from "./utility/config_utility"; // Importing configuration utility functions +} from "./utility/config_utility.js"; // Importing configuration utility functions import { + formatString, getSecretProviders, getSecretscope, getSecretServices, updateTool, validateChoice, -} from "./utility/tool_utility"; // Importing utility functions for secret validation -import { validatorHandleService } from "./handler/validator_handler"; // Importing the validation handler +} from "./utility/tool_utility.js"; // Importing utility functions for secret validation +import { validatorHandleService } from "./handler/validator_handler.js"; // Importing the validation handler -// Call the logging setup function to configure logging -setupLogging(); - -const program = new Command(); // Create a new Commander program instance +/** + * Creates a new instance of the Commander program to build the CLI application. + * @type {Command} + */ +const program = new Command(); -// Configure the CLI program details +/** + * Configure the CLI program details including name, description, and version. + */ program .name("How2Validate Tool") // Set the name of the CLI tool .description("Validate various types of secrets for different services.") // Description of what the tool does @@ -29,18 +44,28 @@ program "Expose the version." ); // Set the version and help flag -const providerChoices = getSecretProviders(); // Get supported secret providers -const serviceChoices = getSecretServices(); // Get supported secret services +/** + * Retrieve the list of supported secret providers. + * @type {string[]} + */ +const providerChoices = getSecretProviders(); + +/** + * Retrieve the list of supported secret services. + * @type {string[]} + */ +const serviceChoices = getSecretServices(); /** * Define CLI options using Commander. - * - secretscope: Option for secret scope - * - provider: Option to specify a provider with validation - * - service: Option to specify a service with validation - * - secret: Option to specify the secret to validate - * - response: Option to check if the secret is active or inactive - * - report: Option to get reports via email (Alpha feature) - * - update: Option to update the tool + * + * - `-secretscope`: Option for secret scope. + * - `-provider`: Option to specify a provider with validation. + * - `-service`: Option to specify a service with validation. + * - `-secret`: Option to specify the secret to validate. + * - `-response`: Option to check if the secret is active or inactive. + * - `-report`: Option to get reports via email (Alpha feature). + * - `--update`: Option to update the tool. */ program .option( @@ -65,16 +90,27 @@ program .option("-report", "Get detailed reports. Receive validated secrets via email [Alpha Feature].", false) .option("--update", "Hack the tool to the latest version."); +export function getProvider(): string[] { + return providerChoices +} + +export function getService(provider:string): string[] { + return getSecretServices(undefined,provider) +} + + /** * Validate the provided secret using the given provider, service, and options. * + * @async + * @function validate * @param {string} provider - The provider to use for validation. * @param {string} service - The service to validate the secret with. * @param {string} secret - The secret that needs to be validated. * @param {boolean} response - Whether to get a response status for the secret. * @param {boolean} report - Whether to generate a report for the validation. - * * @returns {Promise} - A promise that resolves when validation is complete. + * @throws Will throw an error if validation fails. */ export async function validate( provider: string, @@ -85,7 +121,7 @@ export async function validate( ): Promise { console.info("Started validating secret..."); const result = await validatorHandleService( - service, + formatString(service), secret, response, report @@ -97,7 +133,10 @@ export async function validate( * Main function that executes the CLI program logic. * Parses the command-line arguments and performs actions based on the options provided. * + * @async + * @function main * @returns {Promise} - A promise that resolves when the program execution is complete. + * @throws Will throw an error if an unexpected issue occurs during execution. */ async function main(): Promise { program.parse(process.argv); // Parse command-line arguments @@ -160,5 +199,7 @@ async function main(): Promise { } } -// Start the main function and handle any unexpected errors -main().catch((error) => console.error(`Unexpected error: ${error}`)); \ No newline at end of file +/** + * Execute the main function and handle any unexpected errors. + */ +main().catch((error) => console.error(`Unexpected error: ${error}`)); diff --git a/src/js/how2validate/utility/config_utility.ts b/src/js/how2validate/utility/config_utility.ts index 6a98051..1704107 100644 --- a/src/js/how2validate/utility/config_utility.ts +++ b/src/js/how2validate/utility/config_utility.ts @@ -1,5 +1,6 @@ import * as path from "path"; import * as fs from "fs"; +import { fileURLToPath } from 'url'; interface Config { DEFAULT?: { @@ -14,10 +15,24 @@ interface Config { let config: Config | null = null; +// Get the directory name +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + /** - * Initialize the configuration by reading the config.ini file. - * Optionally accepts a custom file path. + * Initializes the configuration by reading the config.ini file. + * This function reads the config file from the provided path or defaults to + * the root configuration file located two levels above the current directory. + * + * @module config_utility + * * @param {string} [configFilePath] - The custom file path for config.ini (optional). + * If not provided, the function will attempt to load the config file from + * the default location. + * + * @example + * initConfig(); // Initializes with default config path + * initConfig('/custom/path/config.ini'); // Initializes with a custom config file path */ export function initConfig(configFilePath?: string) { const resolvedPath = configFilePath || path.resolve(__dirname, "..", "..", "config.ini"); @@ -33,9 +48,18 @@ export function initConfig(configFilePath?: string) { } /** - * Parse the content of the config.ini file and convert it to a Config object. + * Parses the content of the config.ini file and converts it to a Config object. + * This function splits the content into sections and keys, building the Config object. + * + * @private + * * @param {string} content - The content of the config.ini file. - * @returns {Config} - The parsed configuration object. + * @returns {Config} The parsed configuration object. + * + * @example + * const configContent = "[DEFAULT]\npackage_name=my-package\nversion=1.0.0"; + * const config = parseConfigContent(configContent); + * console.log(config.DEFAULT.package_name); // 'my-package' */ function parseConfigContent(content: string): Config { const lines = content.split("\n"); @@ -62,7 +86,18 @@ function parseConfigContent(content: string): Config { return result; } -// Function to get the package name from the DEFAULT section +/** + * Retrieves the package name from the 'DEFAULT' section of the configuration. + * If the configuration has not been initialized, an error is thrown. + * + * @returns {string | undefined} The package name if available, or throws an error if the configuration is not initialized. + * + * @throws {Error} If the configuration is not initialized. + * + * @example + * const packageName = getPackageName(); + * console.log(packageName); // Outputs the package name from config + */ export function getPackageName(): string | undefined { if (config && config.DEFAULT) { return config.DEFAULT.package_name as string; @@ -71,6 +106,18 @@ export function getPackageName(): string | undefined { } } +/** + * Retrieves the active secret status from the 'SECRET' section of the configuration. + * If the configuration has not been initialized, an error is thrown. + * + * @returns {string | undefined} The active secret status or throws an error if the configuration is not initialized. + * + * @throws {Error} If the configuration is not initialized. + * + * @example + * const activeSecret = getActiveSecretStatus(); + * console.log(activeSecret); // Outputs the active secret status from config + */ export function getActiveSecretStatus(): string | undefined { if (config && config.SECRET) { return config.SECRET.secret_active as string; @@ -79,6 +126,18 @@ export function getActiveSecretStatus(): string | undefined { } } +/** + * Retrieves the inactive secret status from the 'SECRET' section of the configuration. + * If the configuration has not been initialized, an error is thrown. + * + * @returns {string | undefined} The inactive secret status or throws an error if the configuration is not initialized. + * + * @throws {Error} If the configuration is not initialized. + * + * @example + * const inactiveSecret = getInactiveSecretStatus(); + * console.log(inactiveSecret); // Outputs the inactive secret status from config + */ export function getInactiveSecretStatus(): string | undefined { if (config && config.SECRET) { return config.SECRET.secret_inactive as string; @@ -87,6 +146,18 @@ export function getInactiveSecretStatus(): string | undefined { } } +/** + * Retrieves the version from the 'DEFAULT' section of the configuration. + * If the configuration has not been initialized, an error is thrown. + * + * @returns {string | undefined} The version if available, or throws an error if the configuration is not initialized. + * + * @throws {Error} If the configuration is not initialized. + * + * @example + * const version = getVersion(); + * console.log(version); // Outputs the version from config + */ export function getVersion(): string | undefined { if (config && config.DEFAULT) { return config.DEFAULT.version as string; diff --git a/src/js/how2validate/utility/log_utility.ts b/src/js/how2validate/utility/log_utility.ts index 4009d4c..5501f6e 100644 --- a/src/js/how2validate/utility/log_utility.ts +++ b/src/js/how2validate/utility/log_utility.ts @@ -1,25 +1,29 @@ -import * as logging from "loglevel"; // Importing the loglevel library for logging functionality +// Importing utility functions to get secret status values from the config import { getActiveSecretStatus, getInactiveSecretStatus, -} from "./config_utility"; // Importing utility functions to get secret status values +} from "./config_utility.js"; /** - * Set up the logging configuration. - * This function initializes the logging level to INFO to control the verbosity of log messages. - * @param {logging.LogLevelDesc} [level='INFO'] - Optional parameter to set the logging level. - */ -export function setupLogging(level: logging.LogLevelDesc = "INFO") { - logging.setLevel(level); // Set logging level, default to INFO -} - -/** - * Generate a message about the status of a secret. - * @param service - The name of the service associated with the secret. - * @param isActive - The current status of the secret (active or inactive). - * @param responseData - Optional data to provide more context if a response exists. - * @returns A formatted message describing the secret's status. - * @throws An error if the isActive value is not recognized. + * Generates a formatted message about the status of a secret. + * This function evaluates whether the secret is active or inactive and constructs + * a corresponding status message, with optional response data for additional context. + * + * @param {string} service - The name of the service associated with the secret. + * @param {string} isActive - The current status of the secret (active or inactive). + * This value is compared against the active/inactive status from the config. + * @param {boolean} [response] - Optional boolean to indicate whether there is a response (not used for formatting). + * @param {any} [responseData] - Optional data to provide additional context, appended to the message if available. + * @returns {string} A formatted message describing the secret's status and response data (if provided). + * + * @throws {Error} If the isActive value is not recognized (neither active nor inactive). + * + * @example + * const service = "Payment Service"; + * const isActive = getActiveSecretStatus(); // or getInactiveSecretStatus() + * const message = getSecretStatusMessage(service, isActive); + * console.log(message); + * // Output: "The provided secret 'Payment Service' is currently active and operational." */ export function getSecretStatusMessage( service: string, @@ -29,14 +33,15 @@ export function getSecretStatusMessage( ): string { let status: string; - // Check if the secret is active or inactive based on provided status + // Check if the secret is active or inactive based on the provided status if (isActive === getActiveSecretStatus()) { status = "active and operational"; // Set status message for active secret } else if (isActive === getInactiveSecretStatus()) { status = "inactive and not operational"; // Set status message for inactive secret } else { + // Throw an error if the status doesn't match the expected active/inactive values throw new Error( - `Unexpected isActive value: ${isActive}. Expected 'Active' or 'InActive'.` + `Unexpected isActive value: ${isActive}. Expected 'Active' or 'Inactive'.` ); } diff --git a/src/js/how2validate/utility/tool_utility.ts b/src/js/how2validate/utility/tool_utility.ts index 415d924..d9fb077 100644 --- a/src/js/how2validate/utility/tool_utility.ts +++ b/src/js/how2validate/utility/tool_utility.ts @@ -1,30 +1,28 @@ import * as fs from "fs"; // Importing the 'fs' module for file system operations import * as path from "path"; // Importing the 'path' module for handling file and directory paths +import { fileURLToPath } from 'url'; // Importing fileURLToPath to convert URL to path import * as logging from "loglevel"; // Importing loglevel for logging messages -import Table from 'cli-table3'; +import Table from 'cli-table3'; // Importing cli-table3 for formatted table display import { execSync } from "child_process"; // Importing execSync to run shell commands synchronously -import { getPackageName } from "./config_utility"; // Importing a function to get the package name from configuration -import { setupLogging } from "./log_utility"; // Importing the function to set up logging +import { getPackageName } from "./config_utility.js"; // Importing a function to get the package name from configuration -// Call the logging setup function to initialize logging configuration -setupLogging(); - -// Get the directory of the current file -const currentDir = __dirname; +// Get the directory name +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); // Define the path to the TokenManager JSON file const tokenManager_filePath = path.join( - currentDir, + __dirname, "..", "..", "tokenManager.json" ); /** - * Get the secret providers from the TokenManager JSON file. - * @param filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). - * @returns An array of enabled secret providers. + * Get the enabled secret providers from the TokenManager JSON file. + * @param {string} filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). + * @returns {string[]} An array of enabled secret providers. */ export function getSecretProviders( filePath: string = tokenManager_filePath @@ -47,18 +45,29 @@ export function getSecretProviders( } /** - * Get the secret services from the TokenManager JSON file. - * @param filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). - * @returns An array of enabled secret services. + * Get the enabled secret services from the TokenManager JSON file. + * @param {string} filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). + * @returns {string[]} An array of enabled secret services. */ export function getSecretServices( - filePath: string = tokenManager_filePath + filePath: string = tokenManager_filePath, + provider: string = "" ): string[] { + // Read the JSON file and parse its contents const data = JSON.parse(fs.readFileSync(filePath, "utf-8")); const enabledSecretsServices: string[] = []; + provider = formatString(provider) - for (const provider in data) { - const tokens = data[provider]; + // Iterate through each provider in the data + for (const currentProvider in data) { + const formatterProvider = formatString(currentProvider) + // If a specific provider is provided, check for a match + if (provider && provider !== formatterProvider) { + continue; // Skip to the next provider if there's no match + } + + const tokens = data[currentProvider]; + // Check each token's enabled status for (const tokenInfo of tokens) { if (tokenInfo.is_enabled) { enabledSecretsServices.push(tokenInfo.display_name); // Add display name of enabled services @@ -69,7 +78,10 @@ export function getSecretServices( return enabledSecretsServices; // Return the list of enabled secret services } -// Function to get and display secret providers and services +/** + * Get and display secret providers and services from the TokenManager JSON file. + * @param {string} filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). + */ export function getSecretscope(filePath: string = tokenManager_filePath): void { // Read and parse the JSON file contents const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); @@ -104,17 +116,20 @@ export function getSecretscope(filePath: string = tokenManager_filePath): void { /** * Format the enabled service providers for display. - * @param filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). - * @returns A formatted string of enabled service providers. + * @param {string} filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). + * @returns {string} A formatted string of enabled service providers. */ export function formatServiceProviders( filePath: string = tokenManager_filePath ): string { + // Read the JSON file and parse its contents const data = JSON.parse(fs.readFileSync(filePath, "utf-8")); const enabledSecretsServices: string[] = []; + // Iterate through each provider in the data for (const provider in data) { const tokens = data[provider]; + // Check each token's enabled status for (const tokenInfo of tokens) { if (tokenInfo.is_enabled) { enabledSecretsServices.push(provider); // Add enabled provider to the list @@ -128,17 +143,20 @@ export function formatServiceProviders( /** * Format the enabled services for display. - * @param filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). - * @returns A formatted string of enabled services with their providers. + * @param {string} filePath - The path to the TokenManager JSON file (default: tokenManager_filePath). + * @returns {string} A formatted string of enabled services with their providers. */ export function formatServices( filePath: string = tokenManager_filePath ): string { + // Read the JSON file and parse its contents const data = JSON.parse(fs.readFileSync(filePath, "utf-8")); const enabledSecretsServices: string[] = []; + // Iterate through each provider in the data for (const provider in data) { const tokens = data[provider]; + // Check each token's enabled status for (const tokenInfo of tokens) { if (tokenInfo.is_enabled) { // Add formatted string of provider and service display name @@ -153,9 +171,9 @@ export function formatServices( /** * Format a string by converting it to lowercase and replacing spaces with underscores. - * @param inputString - The string to format. - * @returns The formatted string. - * @throws An error if the input is not a string. + * @param {string} inputString - The string to format. + * @returns {string} The formatted string. + * @throws {Error} An error if the input is not a string. */ export function formatString(inputString: string): string { if (typeof inputString !== "string") { @@ -167,10 +185,10 @@ export function formatString(inputString: string): string { /** * Validate a user's choice against a list of valid choices. - * @param value - The value to validate. - * @param validChoices - An array of valid choices. - * @returns The formatted value if valid. - * @throws An error if the value is not valid. + * @param {string} value - The value to validate. + * @param {string[]} validChoices - An array of valid choices. + * @returns {string} The formatted value if valid. + * @throws {Error} An error if the value is not valid. */ export function validateChoice(value: string, validChoices: string[]): string { const formattedValue = formatString(value); // Format the user's choice @@ -188,9 +206,9 @@ export function validateChoice(value: string, validChoices: string[]): string { /** * Redact a secret by masking part of it with asterisks. - * @param secret - The secret to redact. - * @returns The redacted secret. - * @throws An error if the input is not a string. + * @param {string} secret - The secret to redact. + * @returns {string} The redacted secret. + * @throws {Error} An error if the input is not a string. */ export function redactSecret(secret: string): string { if (typeof secret !== "string") { @@ -231,41 +249,27 @@ export function updateTool(): void { execSync(`bun add ${packageName} --global`, { stdio: "inherit" }); break; default: - console.error( - "Unsupported package manager. Please install the tool manually." - ); - return; // Exit if the package manager is not recognized + logging.warn("Unknown package manager. Please update manually."); // Log warning for unknown package manager } - console.log("Tool updated to the latest version."); // Log success message } catch (error) { - console.error(`Failed to update the tool: ${error}`); // Log error message if the update fails + logging.error(`Failed to update the tool: ${error}`); // Log error message if update fails } } /** - * Detect the package manager installed on the system. - * @returns The name of the package manager (npm, yarn, pnpm, bun, or unknown). + * Detect the package manager being used in the project. + * @returns {string} The name of the detected package manager. */ function detectPackageManager(): string { - try { - execSync("npm --version", { stdio: "ignore" }); // Check for npm - return "npm"; - } catch { - try { - execSync("yarn --version", { stdio: "ignore" }); // Check for yarn - return "yarn"; - } catch { - try { - execSync("pnpm --version", { stdio: "ignore" }); // Check for pnpm - return "pnpm"; - } catch { - try { - execSync("bun --version", { stdio: "ignore" }); // Check for bun - return "bun"; - } catch { - return "unknown"; // Return 'unknown' if no recognized package manager is found - } - } - } - } + const hasNpm = fs.existsSync(path.join(__dirname, "..", "node_modules")); // Check for npm + const hasYarn = fs.existsSync(path.join(__dirname, "..", "yarn.lock")); // Check for yarn + const hasPnpm = fs.existsSync(path.join(__dirname, "..", "pnpm-lock.yaml")); // Check for pnpm + const hasBun = fs.existsSync(path.join(__dirname, "..", "bun.lockb")); // Check for bun + + if (hasBun) return "bun"; // Return bun if found + if (hasPnpm) return "pnpm"; // Return pnpm if found + if (hasYarn) return "yarn"; // Return yarn if found + if (hasNpm) return "npm"; // Return npm if found + + return "unknown"; // Return unknown if none found } diff --git a/src/js/how2validate/validators/npm/npm_access_token.ts b/src/js/how2validate/validators/npm/npm_access_token.ts index f47cd52..d114460 100644 --- a/src/js/how2validate/validators/npm/npm_access_token.ts +++ b/src/js/how2validate/validators/npm/npm_access_token.ts @@ -1,19 +1,32 @@ +/** + * @module NPM Access Token Validator + * @description + * This module provides functionality to validate NPM Access Token by interacting with the NPM API. + * It checks the validity of the provided token and returns appropriate status messages based on the response. + * + * @requires axios + * @requires ../../utility/config_utility.js + * @requires ../../utility/log_utility.js + */ + import axios, { AxiosError } from "axios"; // Import Axios for making HTTP requests and the AxiosError type for error handling import { getActiveSecretStatus, getInactiveSecretStatus, -} from "../../utility/config_utility"; // Import functions to retrieve active and inactive secret statuses -import { getSecretStatusMessage } from "../../utility/log_utility"; // Import function to format status messages +} from "../../utility/config_utility.js"; // Import functions to retrieve active and inactive secret statuses +import { getSecretStatusMessage } from "../../utility/log_utility.js"; // Import function to format status messages /** * Validate an NPM access token by making an API request to check its validity. + * + * @module npm_access_token * * @param {string} service - The name of the service being validated (in this case, NPM). * @param {string} secret - The NPM access token to validate. * @param {boolean} response - A flag to indicate whether to include detailed response data. * @param {boolean} [report] - An optional flag for additional reporting features. * - * @returns {Promise} - A promise that resolves to a status message about the access token validation. + * @returns {Promise} A promise that resolves to a status message about the access token validation. */ export async function validateNpmAccessToken( service: string, @@ -62,10 +75,12 @@ export async function validateNpmAccessToken( } } + /** - * Handle the case when the NPM access token is inactive or invalid. + * Handle the case when the token is inactive or invalid. * - * @param {string} service - The name of the service being validated (NPM). + * @function handleInactiveStatus + * @param {string} service - The name of the service being validated. * @param {boolean} response - A flag to indicate whether to include detailed response data. * @param {any} [data] - Optional additional data to include in the response. * @@ -97,11 +112,12 @@ function handleInactiveStatus( /** * Handle errors that occur during the validation process. * - * @param {unknown} error - The error object that was thrown during the request. - * @param {string} service - The name of the service being validated (NPM). + * @function handleErrors + * @param {unknown} error - The error object that was thrown. + * @param {string} service - The name of the service being validated. * @param {boolean} response - A flag to indicate whether to include detailed response data. * - * @returns {string} - A formatted status message based on the type of error encountered. + * @returns {string} - A formatted status message based on the type of error. */ function handleErrors( error: unknown, diff --git a/src/js/how2validate/validators/snyk/snyk_auth_key.ts b/src/js/how2validate/validators/snyk/snyk_auth_key.ts index 4acdac1..a7b98a4 100644 --- a/src/js/how2validate/validators/snyk/snyk_auth_key.ts +++ b/src/js/how2validate/validators/snyk/snyk_auth_key.ts @@ -1,19 +1,33 @@ -import axios from "axios"; // Import Axios for making HTTP requests +/** + * @module Snyk Auth Key Validator + * @description + * This module provides functionality to validate Snyk Auth Key by interacting with the Snyk API. + * It checks the validity of the provided token and returns appropriate status messages based on the response. + * + * @requires axios + * @requires ../../utility/config_utility.js + * @requires ../../utility/log_utility.js + */ + +import axios, { AxiosError } from "axios"; // Import Axios for making HTTP requests and AxiosError for error handling import { getActiveSecretStatus, getInactiveSecretStatus, -} from "../../utility/config_utility"; // Import functions to get secret statuses -import { getSecretStatusMessage } from "../../utility/log_utility"; // Import function to format status messages +} from "../../utility/config_utility.js"; // Import functions to get secret statuses +import { getSecretStatusMessage } from "../../utility/log_utility.js"; // Import function to format status messages /** - * Validate a Snyk authentication key by making an API request to check its validity. + * Validate a Snyk auth key by making an API request to check its validity. * + * @async + * @function validateSnykAuthKey * @param {string} service - The name of the service being validated. - * @param {string} secret - The Snyk API key (token) to validate. + * @param {string} secret - The token or secret to validate. * @param {boolean} response - A flag to indicate whether to include detailed response data. * @param {boolean} [report] - An optional flag for additional reporting features. * - * @returns {Promise} - A promise that resolves to a status message about the authentication key validation. + * @returns {Promise} - A promise that resolves to a status message about the token validation. + * @throws {Error} - Throws an error if the validation process encounters an unexpected issue. */ export async function validateSnykAuthKey( service: string, @@ -23,20 +37,18 @@ export async function validateSnykAuthKey( ): Promise { // Snyk API endpoint for retrieving user information const url = "https://snyk.io/api/v1/user"; - - // Headers to prevent caching and authorize the request using the provided API key (token) - const headers = { - "Cache-Control": "no-cache", - Authorization: `token ${secret}`, // Authorization header with the Snyk token - }; - + const nocacheHeaders = { "Cache-Control": "no-cache" }; // Header to prevent caching + const headers = { Authorization: `token ${secret}` }; // Authorization header with the Snyk token + try { - // Send a GET request to the Snyk API to validate the token - const responseData = await axios.get(url, { headers }); + // Make the API request to validate the token + const responseData = await axios.get(url, { + headers: { ...nocacheHeaders, ...headers }, // Combine headers + }); - // Check if the request was successful (HTTP 200) + // Check if the request was successful if (responseData.status === 200) { - // If no detailed response is requested, return the active status message + // If successful and no detailed response is requested if (!response) { return getSecretStatusMessage( service, @@ -44,7 +56,7 @@ export async function validateSnykAuthKey( response ); } else { - // Return the active status message along with the response data + // If successful and detailed response is requested return getSecretStatusMessage( service, getActiveSecretStatus() as string, @@ -53,43 +65,42 @@ export async function validateSnykAuthKey( ); } } else { - // If the response status code is not 200, handle it as an inactive key - return handleInactiveResponse(service, response, responseData.data); + // Handle non-successful status codes + return handleInactiveStatus(service, response, responseData.data); } } catch (error) { - // Handle errors that occur during the request and return the inactive status message - return handleError(service, response, error); + // Handle errors that occur during the request + return handleErrors(error, service, response); } } /** - * Handle the case when the authentication key is inactive or invalid. + * Handle the case when the token is inactive or invalid. * + * @function handleInactiveStatus * @param {string} service - The name of the service being validated. * @param {boolean} response - A flag to indicate whether to include detailed response data. - * @param {any} [responseData] - Optional additional data to include in the response. + * @param {any} [data] - Optional additional data to include in the response. * * @returns {string} - A formatted status message indicating the inactive status. */ -function handleInactiveResponse( +function handleInactiveStatus( service: string, response: boolean, - responseData: any + data?: any ): string { if (!response) { - // Return inactive status message without additional data return getSecretStatusMessage( service, getInactiveSecretStatus() as string, response ); } else { - // Return inactive status message with additional response data return getSecretStatusMessage( service, getInactiveSecretStatus() as string, response, - responseData + data ? JSON.stringify(data) : "No additional data." ); } } @@ -97,24 +108,50 @@ function handleInactiveResponse( /** * Handle errors that occur during the validation process. * + * @function handleErrors + * @param {unknown} error - The error object that was thrown. * @param {string} service - The name of the service being validated. * @param {boolean} response - A flag to indicate whether to include detailed response data. - * @param {any} error - The error object that was thrown. * - * @returns {string} - A formatted status message based on the type of error encountered. + * @returns {string} - A formatted status message based on the type of error. */ -function handleError(service: string, response: boolean, error: any): string { - let errorMessage = ""; - - // Check if the error is an Axios error +function handleErrors( + error: unknown, + service: string, + response: boolean +): string { if (axios.isAxiosError(error)) { - // Extract the error message or response data - errorMessage = error.response ? error.response.data : error.message; - } else { - // Handle general errors that are not Axios errors - errorMessage = error.message; + const axiosError = error as AxiosError; // Cast error to AxiosError for more specific handling + if (!response) { + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response + ); + } else { + // Include detailed error information in the response + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response, + axiosError.response?.data || axiosError.message + ); + } } - // Return inactive response handling with the error message - return handleInactiveResponse(service, response, errorMessage); + // Handle general errors that are not Axios errors + if (!response) { + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response + ); + } else { + return getSecretStatusMessage( + service, + getInactiveSecretStatus() as string, + response, + "An unexpected error occurred." + ); + } } diff --git a/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts b/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts index 83792ae..ec499dc 100644 --- a/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts +++ b/src/js/how2validate/validators/sonarcloud/sonarcloud_token.ts @@ -1,19 +1,33 @@ +/** + * @module SonarCloud Token Validator + * @description + * This module provides functionality to validate SonarCloud tokens by interacting with the SonarCloud API. + * It checks the validity of the provided token and returns appropriate status messages based on the response. + * + * @requires axios + * @requires ../../utility/config_utility.js + * @requires ../../utility/log_utility.js + */ + import axios, { AxiosError } from "axios"; // Import Axios for making HTTP requests and AxiosError for error handling import { getActiveSecretStatus, getInactiveSecretStatus, -} from "../../utility/config_utility"; // Import functions to get secret statuses -import { getSecretStatusMessage } from "../../utility/log_utility"; // Import function to format status messages +} from "../../utility/config_utility.js"; // Import functions to get secret statuses +import { getSecretStatusMessage } from "../../utility/log_utility.js"; // Import function to format status messages /** * Validate a SonarCloud token by making an API request to check its validity. * + * @async + * @function validateSonarcloudToken * @param {string} service - The name of the service being validated. * @param {string} secret - The token or secret to validate. * @param {boolean} response - A flag to indicate whether to include detailed response data. * @param {boolean} [report] - An optional flag for additional reporting features. * * @returns {Promise} - A promise that resolves to a status message about the token validation. + * @throws {Error} - Throws an error if the validation process encounters an unexpected issue. */ export async function validateSonarcloudToken( service: string, @@ -62,6 +76,7 @@ export async function validateSonarcloudToken( /** * Handle the case when the token is inactive or invalid. * + * @function handleInactiveStatus * @param {string} service - The name of the service being validated. * @param {boolean} response - A flag to indicate whether to include detailed response data. * @param {any} [data] - Optional additional data to include in the response. @@ -92,6 +107,7 @@ function handleInactiveStatus( /** * Handle errors that occur during the validation process. * + * @function handleErrors * @param {unknown} error - The error object that was thrown. * @param {string} service - The name of the service being validated. * @param {boolean} response - A flag to indicate whether to include detailed response data. diff --git a/src/js/jsr.json b/src/js/jsr.json index 8d64520..75304c2 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.2-beta.9", + "version": "0.0.2-beta.16", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { diff --git a/src/js/package.json b/src/js/package.json index 343702f..36d572d 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,13 +1,15 @@ { "name": "how2validate", - "version": "0.0.1-beta.6", + "version": "0.0.1-beta.14", + "private": true, "description": "A CLI tool to validate secrets for different services.", "main": "how2validate/index.ts", + "type": "module", "scripts": { - "build": "npx tsc && npx cpx './config.ini' dist/ && npx cpx './tokenManager.json' dist/", + "build": "npm link && npx tsc && npx cpx './config.ini' dist/ && npx cpx './tokenManager.json' dist/", "start": "node dist/index.js", - "publishnpm": "npm publish", - "publishjsr": "npx jsr publish" + "publishnpm": "npm publish --access=restricted", + "publishjsr": "npx jsr publish --allow-dirty" }, "repository": { "type": "git", @@ -25,7 +27,6 @@ "url": "https://github.com/Blackplums/how2validate/issues" }, "homepage": "https://github.com/Blackplums/how2validate#readme", - "type": "commonjs", "dependencies": { "axios": "^1.7.7", "cli-table3": "^0.6.5", @@ -43,7 +44,7 @@ "@types/node": "^22.5.5", "cpx": "^1.2.1", "semantic-release": "^24.1.1", - "typescript": "^5.6.2" + "typescript": "^5.7.0-dev.20240928" }, "files": [ "how2validate/*", diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index d66f2b0..023db99 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "target": "ES2021", // Specifies the JavaScript version to compile to. ES6 (also known as ES2015) is a widely supported version. - "module": "commonjs", // Defines the module system to use. CommonJS is the standard for Node.js modules. + "module": "ESNext", // Defines the module system to use. CommonJS is the standard for Node.js modules. + "lib": ["ESNext"], "outDir": "./dist", // The output directory where compiled JavaScript files will be placed. "rootDir": "./how2validate", // The root directory of your TypeScript source files; all files under this directory will be included in the compilation. "strict": true, // Enables all strict type-checking options for more rigorous checks. From 4fd6872f3bd3b5c0d5af4ef4a95b09ee2f11336f Mon Sep 17 00:00:00 2001 From: VigneshKna Date: Sun, 29 Sep 2024 14:26:31 +0530 Subject: [PATCH 024/106] chore(version): updated versions --- src/js/config.ini | 2 +- src/js/jsr.json | 2 +- src/js/package.json | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/js/config.ini b/src/js/config.ini index 3cec010..3f45c28 100644 --- a/src/js/config.ini +++ b/src/js/config.ini @@ -1,6 +1,6 @@ [DEFAULT] package_name = @how2validate/how2validate -version = 0.0.1-beta.14 +version = 0.0.1-beta.15 [SECRET] secret_active = Active diff --git a/src/js/jsr.json b/src/js/jsr.json index 75304c2..abedfea 100644 --- a/src/js/jsr.json +++ b/src/js/jsr.json @@ -1,6 +1,6 @@ { "name": "@how2validate/how2validate", - "version": "0.0.2-beta.16", + "version": "0.0.2-beta.17", "license": "MIT", "exports": "./how2validate/index.ts", "publish": { diff --git a/src/js/package.json b/src/js/package.json index 36d572d..732402f 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,14 +1,13 @@ { "name": "how2validate", - "version": "0.0.1-beta.14", - "private": true, + "version": "0.0.1-beta.15", "description": "A CLI tool to validate secrets for different services.", "main": "how2validate/index.ts", "type": "module", "scripts": { "build": "npm link && npx tsc && npx cpx './config.ini' dist/ && npx cpx './tokenManager.json' dist/", "start": "node dist/index.js", - "publishnpm": "npm publish --access=restricted", + "publishnpm": "npm publish", "publishjsr": "npx jsr publish --allow-dirty" }, "repository": { From f23dc0a954557761f73e873e88ec6a5717f375bc Mon Sep 17 00:00:00 2001 From: manimarans Date: Sun, 29 Sep 2024 15:49:04 +0530 Subject: [PATCH 025/106] feat(react):base setup ready --- wiki/.npmrc | 1 + wiki/app/globals.css | 79 +++++++++-- wiki/app/page.tsx | 98 +------------ wiki/components.json | 20 +++ wiki/components/how2validate.tsx | 63 +++++++++ wiki/components/sections/faq.tsx | 15 ++ wiki/components/sections/footer.tsx | 12 ++ wiki/components/sections/header.tsx | 34 +++++ wiki/components/sections/tabs.tsx | 10 ++ wiki/components/sections/text-section.tsx | 16 +++ wiki/components/sections/validations.tsx | 42 ++++++ wiki/components/ui/button.tsx | 56 ++++++++ wiki/components/ui/dropdown-select.tsx | 38 +++++ wiki/components/ui/input.tsx | 25 ++++ wiki/components/ui/select.tsx | 160 ++++++++++++++++++++++ wiki/components/ui/switch.tsx | 29 ++++ wiki/components/ui/textarea.tsx | 24 ++++ wiki/lib/utils.ts | 6 + wiki/package.json | 22 ++- wiki/tailwind.config.ts | 60 ++++++-- 20 files changed, 687 insertions(+), 123 deletions(-) create mode 100644 wiki/.npmrc create mode 100644 wiki/components.json create mode 100644 wiki/components/how2validate.tsx create mode 100644 wiki/components/sections/faq.tsx create mode 100644 wiki/components/sections/footer.tsx create mode 100644 wiki/components/sections/header.tsx create mode 100644 wiki/components/sections/tabs.tsx create mode 100644 wiki/components/sections/text-section.tsx create mode 100644 wiki/components/sections/validations.tsx create mode 100644 wiki/components/ui/button.tsx create mode 100644 wiki/components/ui/dropdown-select.tsx create mode 100644 wiki/components/ui/input.tsx create mode 100644 wiki/components/ui/select.tsx create mode 100644 wiki/components/ui/switch.tsx create mode 100644 wiki/components/ui/textarea.tsx create mode 100644 wiki/lib/utils.ts diff --git a/wiki/.npmrc b/wiki/.npmrc new file mode 100644 index 0000000..41583e3 --- /dev/null +++ b/wiki/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/wiki/app/globals.css b/wiki/app/globals.css index 13d40b8..a5cc41b 100644 --- a/wiki/app/globals.css +++ b/wiki/app/globals.css @@ -2,21 +2,7 @@ @tailwind components; @tailwind utilities; -:root { - --background: #ffffff; - --foreground: #171717; -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - body { - color: var(--foreground); - background: var(--background); font-family: Arial, Helvetica, sans-serif; } @@ -25,3 +11,68 @@ body { text-wrap: balance; } } + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/wiki/app/page.tsx b/wiki/app/page.tsx index 433c8aa..e19a18c 100644 --- a/wiki/app/page.tsx +++ b/wiki/app/page.tsx @@ -1,101 +1,7 @@ -import Image from "next/image"; +import { How2validate } from "@/components/how2validate"; export default function Home() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. Save and see your changes instantly.
  4. -
- - -
- -
+ ); } diff --git a/wiki/components.json b/wiki/components.json new file mode 100644 index 0000000..137bc8d --- /dev/null +++ b/wiki/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/wiki/components/how2validate.tsx b/wiki/components/how2validate.tsx new file mode 100644 index 0000000..d88cb8b --- /dev/null +++ b/wiki/components/how2validate.tsx @@ -0,0 +1,63 @@ +"use client" + +import { useState } from "react" +import { Textarea } from "@/components/ui/textarea" +import { Tabs } from "./sections/tabs" +import { TxtSection } from "./sections/text-section" +import { ValidationForm } from "./sections/validations" +import { Foo } from "./sections/footer" +import { Faq } from "./sections/faq" +import { Header } from "./sections/header" + +export function How2validate() { + const [theme, setTheme] = useState<"light" | "dark">("light") + + const toggleTheme = () => { + setTheme(theme === "light" ? "dark" : "light") + } + + return ( +
+
+
+
{toggleTheme()}}/> +
+ +
+
+

How2Validate

+

A CLI tool to validate secrets for different services.

+
+ + + +
+ +
+
+ +
+ + +

Validate Secret

+
+ +
+ +
+ +
+ +
+

Contribute

+