diff --git a/README.md b/README.md index a94ae47..b56ef0f 100644 --- a/README.md +++ b/README.md @@ -2,45 +2,76 @@ The Socket Security CLI was created to enable integrations with other tools like Github Actions, Gitlab, BitBucket, local use cases and more. The tool will get the head scan for the provided repo from Socket, create a new one, and then report any new alerts detected. If there are new alerts against the Socket security policy it'll exit with a non-Zero exit code. - - ## Usage ```` shell -socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--branch BRANCH] [--committer COMMITTER] [--pr-number PR_NUMBER] - [--commit-message COMMIT_MESSAGE] [--default-branch] [--target-path TARGET_PATH] [--scm {api,github,gitlab}] [--sbom-file SBOM_FILE] - [--commit-sha COMMIT_SHA] [--generate-license GENERATE_LICENSE] [-v] [--enable-debug] [--enable-json] [--enable-sarif] [--disable-overview] - [--disable-security-issue] [--files FILES] [--ignore-commit-files] [--timeout] +socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--integration {api,github,gitlab}] [--owner OWNER] [--branch BRANCH] + [--committers [COMMITTERS ...]] [--pr-number PR_NUMBER] [--commit-message COMMIT_MESSAGE] [--commit-sha COMMIT_SHA] + [--target-path TARGET_PATH] [--sbom-file SBOM_FILE] [--files FILES] [--default-branch] [--pending-head] + [--generate-license] [--enable-debug] [--enable-json] [--enable-sarif] [--disable-overview] [--disable-security-issue] + [--allow-unverified] [--ignore-commit-files] [--disable-blocking] [--scm SCM] [--timeout TIMEOUT] ```` If you don't want to provide the Socket API Token every time then you can use the environment variable `SOCKET_SECURITY_API_KEY` - -| Parameter | Alternate Name | Required | Default | Description | -|:-------------------------|:---------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| -h | --help | False | | Show the CLI help message | -| --api-token | | False | | Provides the Socket API Token | -| --repo | | True | | The string name in a git approved name for repositories. | -| --branch | | False | | The string name in a git approved name for branches. | -| --committer | | False | | The string name of the person doing the commit or running the CLI. Can be specified multiple times to have more than one committer | -| --pr-number | | False | 0 | The integer for the PR or MR number | -| --commit-message | | False | | The string for a commit message if there is one | -| --default-branch | | False | False | If the flag is specified this will signal that this is the default branch. This needs to be enabled for a report to update Org Alerts and Org Dependencies | -| --target-path | | False | ./ | This is the path to where the manifest files are location. The tool will recursively search for all supported manifest files | -| --scm | | False | api | This is the mode that the tool is to run in. For local runs `api` would be the mode. Other options are `gitlab` and `github` | -| --generate-license | | False | False | If this flag is specified it will generate a json file with the license per package and license text in the current working directory | -| --version | -v | False | | Prints the version and exits | -| --enable-debug | | False | False | Enables debug messaging for the CLI | -| --sbom-file | | False | False | Creates a JSON file with all dependencies and alerts | -| --commit-sha | | False | | The commit hash for the commit | -| --generate-license | | False | False | If enabled with `--sbom-file` will include license details | -| --enable-json | | False | False | If enabled will change the console output format to JSON | -| --enable-sarif | | False | False | If enabled will change the console output format to SARIF | -| --disable-overview | | False | False | If enabled will disable Dependency Overview comments | -| --disable-security-issue | | False | False | If enabled will disable Security Issue Comments | -| --files | | False | | If provided in the format of `["file1", "file2"]` will be used to determine if there have been supported file changes. This is used if it isn't a git repo and you would like to only run if it supported files have changed. | -| --ignore-commit-files | | False | False | If enabled then the CLI will ignore what files are changed in the commit and look for all manifest files | -| --disable-blocking | | False | False | Disables failing checks and will only exit with an exit code of 0 | +### Parameters + +#### Authentication +| Parameter | Required | Default | Description | +|:-------------|:---------|:--------|:--------------------------------------------------------------------------------------| +| --api-token | False | | Socket Security API token (can also be set via SOCKET_SECURITY_API_KEY env var) | + +#### Repository +| Parameter | Required | Default | Description | +|:-------------|:---------|:--------|:-------------------------------------------------------------------------| +| --repo | False | | Repository name in owner/repo format | +| --integration| False | api | Integration type (api, github, gitlab) | +| --owner | False | | Name of the integration owner, defaults to the socket organization slug | +| --branch | False | "" | Branch name | +| --committers | False | | Committer(s) to filter by | + +#### Pull Request and Commit +| Parameter | Required | Default | Description | +|:----------------|:---------|:--------|:-------------------| +| --pr-number | False | "0" | Pull request number| +| --commit-message| False | | Commit message | +| --commit-sha | False | "" | Commit SHA | + +#### Path and File +| Parameter | Required | Default | Description | +|:-------------|:---------|:--------|:-------------------------------------------| +| --target-path| False | ./ | Target path for analysis | +| --sbom-file | False | | SBOM file path | +| --files | False | [] | Files to analyze (JSON array string) | + +#### Branch and Scan Configuration +| Parameter | Required | Default | Description | +|:---------------|:---------|:--------|:----------------------------------------------------------| +| --default-branch| False | False | Make this branch the default branch | +| --pending-head | False | False | If true, the new scan will be set as the branch's head scan| + +#### Output Configuration +| Parameter | Required | Default | Description | +|:----------------------|:---------|:--------|:---------------------------------------------------------------| +| --generate-license | False | False | Generate license information | +| --enable-debug | False | False | Enable debug logging | +| --enable-json | False | False | Output in JSON format | +| --enable-sarif | False | False | Enable SARIF output of results instead of table or JSON format| +| --disable-overview | False | False | Disable overview output | + +#### Security Configuration +| Parameter | Required | Default | Description | +|:-----------------------|:---------|:--------|:-------------------------------| +| --allow-unverified | False | False | Allow unverified packages | +| --disable-security-issue| False | False | Disable security issue checks | + +#### Advanced Configuration +| Parameter | Required | Default | Description | +|:-------------------|:---------|:--------|:-----------------------------------------------| +| --ignore-commit-files| False | False | Ignore commit files | +| --disable-blocking | False | False | Disable blocking mode | +| --scm | False | api | Source control management type | +| --timeout | False | | Timeout in seconds for API requests | ## Development diff --git a/pyproject.toml b/pyproject.toml index 86ca6ae..58a630a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ 'GitPython', 'packaging', 'python-dotenv', - 'socket-sdk-python>=2.0.4' + 'socket-sdk-python>=2.0.5' ] readme = "README.md" description = "Socket Security CLI for CI/CD" diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index b582e38..59b063c 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,2 +1,2 @@ __author__ = 'socket.dev' -__version__ = '2.0.3' +__version__ = '2.0.4' diff --git a/socketsecurity/config.py b/socketsecurity/config.py index 630acca..ae4e169 100644 --- a/socketsecurity/config.py +++ b/socketsecurity/config.py @@ -23,6 +23,7 @@ class CliConfig: enable_debug: bool = False allow_unverified: bool = False enable_json: bool = False + enable_sarif: bool = False disable_overview: bool = False disable_security_issue: bool = False files: str = "[]" @@ -31,7 +32,7 @@ class CliConfig: integration_type: IntegrationType = "api" integration_org_slug: Optional[str] = None pending_head: bool = False - timeout: Optional[int] = None + timeout: Optional[int] = 1200 @classmethod def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': parser = create_argument_parser() @@ -61,6 +62,7 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': 'enable_debug': args.enable_debug, 'allow_unverified': args.allow_unverified, 'enable_json': args.enable_json, + 'enable_sarif': args.enable_sarif, 'disable_overview': args.disable_overview, 'disable_security_issue': args.disable_security_issue, 'files': args.files, @@ -215,6 +217,7 @@ def create_argument_parser() -> argparse.ArgumentParser: config_group.add_argument( "--default_branch", dest="default_branch", + action="store_true", help=argparse.SUPPRESS ) config_group.add_argument( @@ -226,6 +229,7 @@ def create_argument_parser() -> argparse.ArgumentParser: config_group.add_argument( "--pending_head", dest="pending_head", + action="store_true", help=argparse.SUPPRESS ) @@ -240,6 +244,7 @@ def create_argument_parser() -> argparse.ArgumentParser: output_group.add_argument( "--generate_license", dest="generate_license", + action="store_true", help=argparse.SUPPRESS ) output_group.add_argument( @@ -251,6 +256,7 @@ def create_argument_parser() -> argparse.ArgumentParser: output_group.add_argument( "--enable_debug", dest="enable_debug", + action="store_true", help=argparse.SUPPRESS ) output_group.add_argument( @@ -260,9 +266,10 @@ def create_argument_parser() -> argparse.ArgumentParser: help="Output in JSON format" ) output_group.add_argument( - "--enable_json", - dest="enable_json", - help=argparse.SUPPRESS + "--enable-sarif", + dest="enable_sarif", + action="store_true", + help="Enable SARIF output of results instead of table or JSON format" ) output_group.add_argument( "--disable-overview", @@ -273,6 +280,7 @@ def create_argument_parser() -> argparse.ArgumentParser: output_group.add_argument( "--disable_overview", dest="disable_overview", + action="store_true", help=argparse.SUPPRESS ) @@ -292,6 +300,7 @@ def create_argument_parser() -> argparse.ArgumentParser: security_group.add_argument( "--disable_security_issue", dest="disable_security_issue", + action="store_true", help=argparse.SUPPRESS ) @@ -306,6 +315,7 @@ def create_argument_parser() -> argparse.ArgumentParser: advanced_group.add_argument( "--ignore_commit_files", dest="ignore_commit_files", + action="store_true", help=argparse.SUPPRESS ) advanced_group.add_argument( @@ -317,6 +327,7 @@ def create_argument_parser() -> argparse.ArgumentParser: advanced_group.add_argument( "--disable_blocking", dest="disable_blocking", + action="store_true", help=argparse.SUPPRESS ) advanced_group.add_argument( diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index 751c456..18d92d7 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -427,7 +427,7 @@ def create_new_diff( no_change: If True, return empty diff """ - print(f"starting create_new_diff with no_change: {no_change}") + log.debug(f"starting create_new_diff with no_change: {no_change}") if no_change: return Diff(id="no_diff_id") @@ -435,7 +435,7 @@ def create_new_diff( files = self.find_files(path) files_for_sending = self.load_files_for_sending(files, path) - print(f"files: {files} found at path {path}") + log.debug(f"files: {files} found at path {path}") if not files: return Diff(id="no_diff_id") diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index ee17772..ca90c27 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -192,6 +192,13 @@ def create_security_comment_sarif(diff) -> dict: Create SARIF-compliant output from the diff report, including dynamic URL generation based on manifest type and improved
formatting for GitHub SARIF display. """ + scan_failed = False + if len(diff.new_alerts) == 0: + for alert in diff.new_alerts: + alert: Issue + if alert.error: + scan_failed = True + break sarif_data = { "$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", diff --git a/socketsecurity/core/socket_config.py b/socketsecurity/core/socket_config.py index f7580df..9e0726c 100644 --- a/socketsecurity/core/socket_config.py +++ b/socketsecurity/core/socket_config.py @@ -9,7 +9,7 @@ class SocketConfig: api_key: str api_url: str = "https://api.socket.dev/v0" - timeout: int = 30 + timeout: int = 1200 allow_unverified_ssl: bool = False org_id: Optional[str] = None org_slug: Optional[str] = None diff --git a/socketsecurity/output.py b/socketsecurity/output.py index ccbf359..e4d3649 100644 --- a/socketsecurity/output.py +++ b/socketsecurity/output.py @@ -5,33 +5,39 @@ from typing import Any, Dict, Optional from .core.messages import Messages from .core.classes import Diff, Issue +from .config import CliConfig class OutputHandler: - blocking_disabled: bool + config: CliConfig logger: logging.Logger - def __init__(self, blocking_disabled: bool): - self.blocking_disabled = blocking_disabled + def __init__(self, config: CliConfig): + self.config = config self.logger = logging.getLogger("socketcli") - def handle_output(self, diff_report: Diff, sbom_file_name: Optional[str] = None, json_output: bool = False) -> int: - """Main output handler that determines output format and returns exit code""" - if json_output: - self.output_console_json(diff_report, sbom_file_name) + def handle_output(self, diff_report: Diff) -> None: + """Main output handler that determines output format""" + if self.config.enable_json: + self.output_console_json(diff_report, self.config.sbom_file) + elif self.config.enable_sarif: + self.output_console_sarif(diff_report, self.config.sbom_file) else: - self.output_console_comments(diff_report, sbom_file_name) + self.output_console_comments(diff_report, self.config.sbom_file) - self.save_sbom_file(diff_report, sbom_file_name) + self.save_sbom_file(diff_report, self.config.sbom_file) def return_exit_code(self, diff_report: Diff) -> int: - if not self.report_pass(diff_report) and not self.blocking_disabled: + if self.config.disable_blocking: + return 0 + + if not self.report_pass(diff_report): return 1 - elif len(diff_report.new_alerts) > 0 and not self.blocking_disabled: + + if len(diff_report.new_alerts) > 0: # 5 means warning alerts but no blocking alerts return 5 - else: - return 0 + return 0 def output_console_comments(self, diff_report: Diff, sbom_file_name: Optional[str] = None) -> None: """Outputs formatted console comments""" @@ -46,15 +52,26 @@ def output_console_comments(self, diff_report: Diff, sbom_file_name: Optional[st def output_console_json(self, diff_report: Diff, sbom_file_name: Optional[str] = None) -> None: """Outputs JSON formatted results""" console_security_comment = Messages.create_security_comment_json(diff_report) + self.save_sbom_file(diff_report, sbom_file_name) self.logger.info(json.dumps(console_security_comment)) + def output_console_sarif(self, diff_report: Diff, sbom_file_name: Optional[str] = None) -> None: + """ + Generate SARIF output from the diff report and print to console. + """ + if diff_report.id != "NO_DIFF_RAN": + # Generate the SARIF structure using Messages + console_security_comment = Messages.create_security_comment_sarif(diff_report) + self.save_sbom_file(diff_report, sbom_file_name) + # Print the SARIF output to the console in JSON format + print(json.dumps(console_security_comment, indent=2)) def report_pass(self, diff_report: Diff) -> bool: """Determines if the report passes security checks""" if not diff_report.new_alerts: return True - if self.blocking_disabled: + if self.config.disable_blocking: return True return not any(issue.error for issue in diff_report.new_alerts) diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index 3ae28fc..cf4f6f5 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -45,11 +45,11 @@ def cli(): def main_code(): config = CliConfig.from_args() - print(f"config: {config.to_dict()}") - output_handler = OutputHandler(blocking_disabled=config.disable_blocking) + log.debug(f"config: {config.to_dict()}") + output_handler = OutputHandler(config) sdk = socketdev(token=config.api_token) - print("sdk loaded") + log.debug("sdk loaded") if config.enable_debug: set_debug_mode(True) @@ -64,13 +64,13 @@ def main_code(): socket_config = SocketConfig( api_key=config.api_token, allow_unverified_ssl=config.allow_unverified, - timeout=config.timeout if config.timeout is not None else 30 # Use CLI timeout if provided + timeout=config.timeout if config.timeout is not None else 1200 # Use CLI timeout if provided ) - print("loaded socket_config") + log.debug("loaded socket_config") client = CliClient(socket_config) - print("loaded client") + log.debug("loaded client") core = Core(socket_config, sdk) - print("loaded core") + log.debug("loaded core") # Load files - files defaults to "[]" in CliConfig try: files = json.loads(config.files) # Will always succeed with empty list by default @@ -135,7 +135,7 @@ def main_code(): should_skip_scan = False # Force scan if ignoring commit files elif files_to_check: # If we have any files to check should_skip_scan = not core.has_manifest_files(list(files_to_check)) - print(f"in elif, should_skip_scan: {should_skip_scan}") + log.debug(f"in elif, should_skip_scan: {should_skip_scan}") if should_skip_scan: log.debug("No manifest files found in changes, skipping scan") @@ -240,14 +240,11 @@ def main_code(): log.info("Starting non-PR/MR flow") diff = core.create_new_diff(config.target_path, params, no_change=should_skip_scan) - output_handler.handle_output(diff, config.sbom_file, config.enable_json) + output_handler.handle_output(diff) else: log.info("API Mode") diff = core.create_new_diff(config.target_path, params, no_change=should_skip_scan) - if config.enable_json: - output_handler.output_console_json(diff, config.sbom_file) - else: - output_handler.output_console_comments(diff, config.sbom_file) + output_handler.handle_output(diff) # Handle license generation if diff is not None and config.generate_license: