From 9e2b6ca30a0f961ae90e0c02de26482a431201db Mon Sep 17 00:00:00 2001 From: Jonathan Mucha Date: Mon, 12 Jan 2026 10:35:26 -0500 Subject: [PATCH 1/2] feat: add GitLab Security Dashboard integration with Dependency Scanning report output Adds support for generating GitLab-compatible Dependency Scanning reports that integrate with GitLab's Security Dashboard. This feature enables Socket security findings to be displayed natively in GitLab merge requests and security dashboards. Key Features: - New --enable-gitlab-security flag to generate GitLab reports - New --gitlab-security-file flag for custom output paths (default: gl-dependency-scanning-report.json) - Generates GitLab Dependency Scanning schema v15.0.0 compliant reports - Supports multiple simultaneous output formats (JSON, SARIF, GitLab) - Includes actionable security alerts (error/warn level) in vulnerability reports - Maps Socket severity levels to GitLab severity (Critical, High, Medium, Low) - Extracts CVE identifiers and dependency chain information - Generates deterministic UUIDs for vulnerability tracking Implementation: - Added GitLab report generator in messages.py with helper functions for severity mapping, identifier extraction, and location parsing - Refactored OutputHandler to support multiple simultaneous output formats - Added comprehensive unit tests (test_gitlab_format.py) and integration tests - Updated documentation with usage examples, CI/CD integration guide, and alert filtering details Co-Authored-By: Claude Sonnet 4.5 --- README.md | 172 +++++++++++++- socketsecurity/config.py | 17 ++ socketsecurity/core/messages.py | 181 ++++++++++++++ socketsecurity/output.py | 58 ++++- tests/unit/test_gitlab_format.py | 393 +++++++++++++++++++++++++++++++ tests/unit/test_output.py | 162 ++++++++++++- 6 files changed, 970 insertions(+), 13 deletions(-) create mode 100644 tests/unit/test_gitlab_format.py diff --git a/README.md b/README.md index f0d3c27..99e21d3 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,32 @@ This will: - Create a repository in Socket named like `my-repo-mobile-web` - Preserve git context (commits, branch info) from the repository root +**Generate GitLab Security Dashboard report:** +```bash +socketcli --enable-gitlab-security \ + --repo owner/repo \ + --target-path . +``` + +This will: +- Scan all manifest files in the current directory +- Generate a GitLab-compatible Dependency Scanning report +- Save to `gl-dependency-scanning-report.json` +- Include all actionable security alerts (error/warn level) + +**Multiple output formats:** +```bash +socketcli --enable-json \ + --enable-sarif \ + --enable-gitlab-security \ + --repo owner/repo +``` + +This will simultaneously generate: +- JSON output to console +- SARIF format to console +- GitLab Security Dashboard report to file + ### Requirements - Both `--sub-path` and `--workspace-name` must be specified together @@ -88,14 +114,15 @@ This will: ## Usage ```` shell -socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--repo-is-public] [--branch BRANCH] [--integration {api,github,gitlab,azure,bitbucket}] - [--owner OWNER] [--pr-number PR_NUMBER] [--commit-message COMMIT_MESSAGE] [--commit-sha COMMIT_SHA] [--committers [COMMITTERS ...]] +socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--repo-is-public] [--branch BRANCH] [--integration {api,github,gitlab,azure,bitbucket}] + [--owner OWNER] [--pr-number PR_NUMBER] [--commit-message COMMIT_MESSAGE] [--commit-sha COMMIT_SHA] [--committers [COMMITTERS ...]] [--target-path TARGET_PATH] [--sbom-file SBOM_FILE] [--license-file-name LICENSE_FILE_NAME] [--save-submitted-files-list SAVE_SUBMITTED_FILES_LIST] - [--save-manifest-tar SAVE_MANIFEST_TAR] [--files FILES] [--sub-path SUB_PATH] [--workspace-name WORKSPACE_NAME] - [--excluded-ecosystems EXCLUDED_ECOSYSTEMS] [--default-branch] [--pending-head] [--generate-license] [--enable-debug] - [--enable-json] [--enable-sarif] [--disable-overview] [--exclude-license-details] [--allow-unverified] [--disable-security-issue] - [--ignore-commit-files] [--disable-blocking] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders] - [--reach] [--reach-version REACH_VERSION] [--reach-analysis-timeout REACH_ANALYSIS_TIMEOUT] + [--save-manifest-tar SAVE_MANIFEST_TAR] [--files FILES] [--sub-path SUB_PATH] [--workspace-name WORKSPACE_NAME] + [--excluded-ecosystems EXCLUDED_ECOSYSTEMS] [--default-branch] [--pending-head] [--generate-license] [--enable-debug] + [--enable-json] [--enable-sarif] [--enable-gitlab-security] [--gitlab-security-file ] + [--disable-overview] [--exclude-license-details] [--allow-unverified] [--disable-security-issue] + [--ignore-commit-files] [--disable-blocking] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders] + [--reach] [--reach-version REACH_VERSION] [--reach-analysis-timeout REACH_ANALYSIS_TIMEOUT] [--reach-analysis-memory-limit REACH_ANALYSIS_MEMORY_LIMIT] [--reach-ecosystems REACH_ECOSYSTEMS] [--reach-exclude-paths REACH_EXCLUDE_PATHS] [--reach-min-severity {low,medium,high,critical}] [--reach-skip-cache] [--reach-disable-analytics] [--reach-output-file REACH_OUTPUT_FILE] [--only-facts-file] [--version] @@ -154,6 +181,8 @@ If you don't want to provide the Socket API Token every time then you can use th | --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 | +| --enable-gitlab-security | False | False | Enable GitLab Security Dashboard output format (Dependency Scanning report) | +| --gitlab-security-file | False | gl-dependency-scanning-report.json | Output file path for GitLab Security report | | --disable-overview | False | False | Disable overview output | | --exclude-license-details | False | False | Exclude license details from the diff report (boosts performance for large repos) | | --version | False | False | Show program's version number and exit | @@ -530,9 +559,136 @@ The manifest archive feature is useful for: ### Differential scan skipped on octopus merge -When your repo uses an **octopus merge** (3+ parents), the CLI may not detect all changed files. +When your repo uses an **octopus merge** (3+ parents), the CLI may not detect all changed files. This is expected Git behavior: the default diff only compares the merge result to the first parent. +## GitLab Security Dashboard Integration + +Socket CLI can generate reports compatible with GitLab's Security Dashboard, allowing vulnerability information to be displayed directly in merge requests and security dashboards. This feature complements the existing [Socket GitLab integration](https://docs.socket.dev/docs/gitlab) by providing standardized dependency scanning reports. + +### Generating GitLab Security Reports + +To generate a GitLab-compatible security report: + +```bash +socketcli --enable-gitlab-security --repo owner/repo +``` + +This creates a `gl-dependency-scanning-report.json` file following GitLab's Dependency Scanning report schema. + +### GitLab CI/CD Integration + +Add Socket Security scanning to your GitLab CI pipeline to generate Security Dashboard reports: + +```yaml +# .gitlab-ci.yml +socket_security_scan: + stage: security + image: python:3.11 + before_script: + - pip install socketsecurity + script: + - socketcli + --api-token $SOCKET_API_TOKEN + --repo $CI_PROJECT_PATH + --branch $CI_COMMIT_REF_NAME + --commit-sha $CI_COMMIT_SHA + --enable-gitlab-security + artifacts: + reports: + dependency_scanning: gl-dependency-scanning-report.json + paths: + - gl-dependency-scanning-report.json + expire_in: 1 week + only: + - merge_requests + - main +``` + +**Note**: This Security Dashboard integration can be used alongside the [Socket GitLab App](https://docs.socket.dev/docs/gitlab) for comprehensive protection: +- **Socket GitLab App**: Real-time PR comments, policy enforcement, and blocking +- **Security Dashboard**: Centralized vulnerability tracking and reporting in GitLab's native interface + +### Custom Output Path + +Specify a custom output path for the GitLab security report: + +```bash +socketcli --enable-gitlab-security --gitlab-security-file custom-path.json +``` + +### Multiple Output Formats + +GitLab security reports can be generated alongside other output formats: + +```bash +socketcli --enable-json --enable-gitlab-security --enable-sarif +``` + +This command will: +- Output JSON format to console +- Save GitLab Security Dashboard report to `gl-dependency-scanning-report.json` +- Save SARIF report (if configured) + +### Security Dashboard Features + +The GitLab Security Dashboard will display: +- **Vulnerability Severity**: Critical, High, Medium, Low levels +- **Affected Packages**: Package name, version, and ecosystem +- **CVE Identifiers**: Direct links to CVE databases when available +- **Dependency Chains**: Distinction between direct and transitive dependencies +- **Remediation Suggestions**: Fix recommendations from Socket Security +- **Alert Categories**: Supply chain risks, malware, vulnerabilities, and more + +### Alert Filtering + +The GitLab report includes **actionable security alerts** based on your Socket policy configuration: + +**Included Alerts** ✅: +- **Error-level alerts** (`error: true`) - Security policy violations that block merges +- **Warning-level alerts** (`warn: true`) - Important security concerns requiring attention + +**Excluded Alerts** ❌: +- **Ignored alerts** (`ignore: true`) - Alerts explicitly ignored in your policy +- **Monitor-only alerts** (`monitor: true` without error/warn) - Tracked but not actionable + +**Socket Alert Types Detected**: +- Supply chain risks (malware, typosquatting, suspicious behavior) +- Security vulnerabilities (CVEs, unsafe code patterns) +- Risky permissions (network access, filesystem access, shell access) +- License policy violations + +All alert types are included in the GitLab report if they're marked as `error` or `warn` by your Socket Security policy, ensuring the Security Dashboard shows only actionable findings. + +### Report Schema + +Socket CLI generates reports compliant with [GitLab Dependency Scanning schema version 15.0.0](https://docs.gitlab.com/ee/development/integrations/secure.html). The reports include: + +- **Scan metadata**: Analyzer and scanner information +- **Vulnerabilities**: Detailed vulnerability data with: + - Unique deterministic UUIDs for tracking + - Package location and dependency information + - Severity levels mapped from Socket's analysis + - Socket-specific alert types and CVE identifiers + - Links to Socket.dev for detailed analysis + +### Requirements + +- **GitLab Version**: GitLab 12.0 or later (for Security Dashboard support) +- **Socket API Token**: Set via `$SOCKET_API_TOKEN` environment variable or `--api-token` parameter +- **CI/CD Artifacts**: Reports must be uploaded as `dependency_scanning` artifacts + +### Troubleshooting + +**Report not appearing in Security Dashboard:** +- Verify the artifact is correctly configured in `.gitlab-ci.yml` +- Check that the job succeeded and artifacts were uploaded +- Ensure the report file follows the correct schema format + +**Empty vulnerabilities array:** +- This is normal if no new security issues were detected +- Check Socket.dev dashboard for full analysis details + ## Development This project uses `pyproject.toml` as the primary dependency specification. diff --git a/socketsecurity/config.py b/socketsecurity/config.py index b32a3d2..ed9d1ec 100644 --- a/socketsecurity/config.py +++ b/socketsecurity/config.py @@ -40,6 +40,8 @@ class CliConfig: allow_unverified: bool = False enable_json: bool = False enable_sarif: bool = False + enable_gitlab_security: bool = False + gitlab_security_file: Optional[str] = None disable_overview: bool = False disable_security_issue: bool = False files: str = None @@ -118,6 +120,8 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': 'allow_unverified': args.allow_unverified, 'enable_json': args.enable_json, 'enable_sarif': args.enable_sarif, + 'enable_gitlab_security': args.enable_gitlab_security, + 'gitlab_security_file': args.gitlab_security_file, 'disable_overview': args.disable_overview, 'disable_security_issue': args.disable_security_issue, 'files': args.files, @@ -449,6 +453,19 @@ def create_argument_parser() -> argparse.ArgumentParser: action="store_true", help="Enable SARIF output of results instead of table or JSON format" ) + output_group.add_argument( + "--enable-gitlab-security", + dest="enable_gitlab_security", + action="store_true", + help="Enable GitLab Security Dashboard output format (Dependency Scanning report)" + ) + output_group.add_argument( + "--gitlab-security-file", + dest="gitlab_security_file", + metavar="", + default="gl-dependency-scanning-report.json", + help="Output file path for GitLab Security report (default: gl-dependency-scanning-report.json)" + ) output_group.add_argument( "--disable-overview", dest="disable_overview", diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index 5678bad..43033a2 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -2,6 +2,8 @@ import logging import os import re +import uuid +from datetime import datetime from pathlib import Path from mdutils import MdUtils from prettytable import PrettyTable @@ -387,6 +389,185 @@ def create_security_comment_json(diff: Diff) -> dict: output["new_alerts"].append(json.loads(str(alert))) return output + @staticmethod + def map_socket_severity_to_gitlab(severity: str) -> str: + """ + Map Socket severity levels to GitLab severity levels. + + Socket: critical, high, medium/middle, low + GitLab: Critical, High, Medium, Low, Info, Unknown + """ + severity_mapping = { + "critical": "Critical", + "high": "High", + "medium": "Medium", + "middle": "Medium", # Socket's older format + "low": "Low", + } + return severity_mapping.get(severity.lower(), "Unknown") + + @staticmethod + def generate_uuid_from_alert_gitlab(alert: Issue) -> str: + """ + Generate deterministic UUID for vulnerability based on alert properties. + This ensures consistent IDs across runs for the same vulnerability. + """ + # Create unique string from alert key properties + unique_str = f"{alert.pkg_name}:{alert.pkg_version}:{alert.type}:{alert.severity}" + + # Generate UUID5 (deterministic) from namespace and unique string + namespace = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') # DNS namespace + return str(uuid.uuid5(namespace, unique_str)) + + @staticmethod + def extract_identifiers_gitlab(alert: Issue) -> list: + """ + Extract CVE and other identifiers from alert properties. + + Socket stores CVE info in alert.props dict. + Returns array of identifier objects per GitLab schema. + """ + identifiers = [] + + # Primary identifier: Socket alert type + identifiers.append({ + "type": "socket_alert", + "name": f"Socket {alert.type}", + "value": alert.type, + "url": alert.url if hasattr(alert, 'url') and alert.url else None + }) + + # Extract CVE identifiers from props + if hasattr(alert, 'props') and alert.props: + if 'cve' in alert.props: + cves = alert.props['cve'] + if isinstance(cves, list): + for cve in cves: + identifiers.append({ + "type": "cve", + "name": cve, + "value": cve, + "url": f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={cve}" + }) + elif isinstance(cves, str): + identifiers.append({ + "type": "cve", + "name": cves, + "value": cves, + "url": f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={cves}" + }) + + return identifiers + + @staticmethod + def extract_location_gitlab(alert: Issue) -> dict: + """ + Extract location information for GitLab report. + + GitLab location requires: + - file: path to manifest file + - dependency: package name and version + - dependency_path (optional): dependency chain + """ + # Get manifest file from introduced_by or manifests attribute + manifest_file = "unknown" + dependency_path = [] + is_direct = True + + if hasattr(alert, 'introduced_by') and alert.introduced_by: + if isinstance(alert.introduced_by, list) and len(alert.introduced_by) > 0: + first_entry = alert.introduced_by[0] + if isinstance(first_entry, (list, tuple)) and len(first_entry) >= 2: + dependency_path_str = first_entry[0] + manifest_file = first_entry[1].split(';')[0] if ';' in first_entry[1] else first_entry[1] + + # Parse dependency path + if ' > ' in dependency_path_str: + dependency_path = dependency_path_str.split(' > ') + # If there's a chain, it's transitive (not direct) + is_direct = len(dependency_path) <= 1 + + elif hasattr(alert, 'manifests') and alert.manifests: + manifest_file = alert.manifests.split(';')[0] + + location = { + "file": manifest_file, + "dependency": { + "package": { + "name": alert.pkg_name + }, + "version": alert.pkg_version, + "direct": is_direct + } + } + + return location + + @staticmethod + def create_security_comment_gitlab(diff: Diff) -> dict: + """ + Create GitLab Dependency Scanning report format from Socket scan results. + + Spec: https://docs.gitlab.com/ee/development/integrations/secure.html + Schema: https://gitlab.com/gitlab-org/security-products/security-report-schemas + + Args: + diff: Diff report containing new_alerts and package information + + Returns: + Dictionary compliant with GitLab Dependency Scanning schema + """ + from socketsecurity import __version__ + + gitlab_report = { + "version": "15.0.0", # GitLab schema version + "scan": { + "analyzer": { + "id": "socket-security", + "name": "Socket Security", + "version": __version__, + "vendor": { + "name": "Socket" + } + }, + "scanner": { + "id": "socket-cli", + "name": "Socket CLI", + "version": __version__, + "vendor": { + "name": "Socket" + } + }, + "type": "dependency_scanning", + "start_time": datetime.utcnow().isoformat() + "Z", + "end_time": datetime.utcnow().isoformat() + "Z", + "status": "success" + }, + "vulnerabilities": [] + } + + # Process each alert + for alert in diff.new_alerts: + vulnerability = { + "id": Messages.generate_uuid_from_alert_gitlab(alert), + "category": "dependency_scanning", + "name": alert.title if hasattr(alert, 'title') else f"{alert.type} in {alert.pkg_name}", + "message": f"{alert.pkg_name}@{alert.pkg_version}: {alert.title if hasattr(alert, 'title') else alert.type}", + "description": alert.description if hasattr(alert, 'description') and alert.description else "", + "severity": Messages.map_socket_severity_to_gitlab(alert.severity), + "identifiers": Messages.extract_identifiers_gitlab(alert), + "links": [{"url": alert.url}] if hasattr(alert, 'url') and alert.url else [], + "location": Messages.extract_location_gitlab(alert) + } + + # Add solution if available + if hasattr(alert, 'suggestion') and alert.suggestion: + vulnerability["solution"] = alert.suggestion + + gitlab_report["vulnerabilities"].append(vulnerability) + + return gitlab_report + @staticmethod def security_comment_template(diff: Diff, config=None) -> str: """ diff --git a/socketsecurity/output.py b/socketsecurity/output.py index 2948bb2..87f3647 100644 --- a/socketsecurity/output.py +++ b/socketsecurity/output.py @@ -19,12 +19,28 @@ def __init__(self, config: CliConfig, sdk: socketdev): def handle_output(self, diff_report: Diff) -> None: """Main output handler that determines output format""" + # Determine which formats to output + formats_to_output = [] + 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: + formats_to_output.append('json') + if self.config.enable_sarif: + formats_to_output.append('sarif') + if self.config.enable_gitlab_security: + formats_to_output.append('gitlab') + + # If no format specified, default to console comments + if not formats_to_output: self.output_console_comments(diff_report, self.config.sbom_file) + else: + # Output all enabled formats + for format_type in formats_to_output: + if format_type == 'json': + self.output_console_json(diff_report, self.config.sbom_file) + elif format_type == 'sarif': + self.output_console_sarif(diff_report, self.config.sbom_file) + elif format_type == 'gitlab': + self.output_gitlab_security(diff_report) if self.config.jira_plugin.enabled: jira_config = { "enabled": self.config.jira_plugin.enabled, @@ -124,6 +140,40 @@ def save_sbom_file(self, diff_report: Diff, sbom_file_name: Optional[str] = None with open(sbom_path, "w") as f: json.dump(diff_report.sbom, f, indent=2) + def output_gitlab_security(self, diff_report: Diff) -> None: + """ + Generate GitLab Security Dashboard (Dependency Scanning) output + and save to file. + + Args: + diff_report: Diff report containing vulnerability data + """ + if diff_report.id != "NO_DIFF_RAN": + # Generate GitLab report structure + gitlab_report = Messages.create_security_comment_gitlab(diff_report) + + # Determine output file path + output_path = self.config.gitlab_security_file or "gl-dependency-scanning-report.json" + + # Save to file + self.save_gitlab_security_file(gitlab_report, output_path) + + self.logger.info(f"GitLab Security report saved to {output_path}") + + def save_gitlab_security_file(self, report: dict, file_path: str) -> None: + """ + Save GitLab Security Dashboard report to file. + + Args: + report: GitLab report dictionary + file_path: Path to save the report file + """ + gitlab_path = Path(file_path) + gitlab_path.parent.mkdir(parents=True, exist_ok=True) + + with open(gitlab_path, "w") as f: + json.dump(report, f, indent=2) + def _output_issue(self, issue: Issue) -> None: """Helper method to format and output a single issue""" severity = issue.severity.upper() if issue.severity else "UNKNOWN" diff --git a/tests/unit/test_gitlab_format.py b/tests/unit/test_gitlab_format.py new file mode 100644 index 0000000..1799dcf --- /dev/null +++ b/tests/unit/test_gitlab_format.py @@ -0,0 +1,393 @@ +import pytest +from socketsecurity.core.messages import Messages +from socketsecurity.core.classes import Diff, Issue + + +class TestGitLabFormat: + """Test suite for GitLab Security Dashboard format generation""" + + def test_gitlab_report_structure(self): + """Test basic GitLab report structure is valid""" + diff = Diff() + diff.new_alerts = [] + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + report = Messages.create_security_comment_gitlab(diff) + + # Verify required top-level fields + assert "version" in report + assert "scan" in report + assert "vulnerabilities" in report + + # Verify scan structure + assert report["scan"]["type"] == "dependency_scanning" + assert "analyzer" in report["scan"] + assert "scanner" in report["scan"] + assert report["scan"]["analyzer"]["id"] == "socket-security" + assert report["scan"]["scanner"]["id"] == "socket-cli" + assert report["scan"]["status"] == "success" + + def test_vulnerability_mapping(self): + """Test Socket Issue maps correctly to GitLab vulnerability""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + test_issue = Issue( + pkg_name="test-package", + pkg_version="1.0.0", + severity="high", + title="Test Vulnerability", + description="Test description", + type="malware", + url="https://socket.dev/test", + manifests="package.json", + props={"cve": ["CVE-2024-1234"]}, + pkg_type="npm", + key="test-key", + purl="pkg:npm/test-package@1.0.0" + ) + diff.new_alerts = [test_issue] + + report = Messages.create_security_comment_gitlab(diff) + + assert len(report["vulnerabilities"]) == 1 + vuln = report["vulnerabilities"][0] + + assert vuln["category"] == "dependency_scanning" + assert vuln["name"] == "Test Vulnerability" + assert vuln["severity"] == "High" + assert vuln["location"]["file"] == "package.json" + assert vuln["location"]["dependency"]["package"]["name"] == "test-package" + assert vuln["location"]["dependency"]["version"] == "1.0.0" + assert vuln["message"] == "test-package@1.0.0: Test Vulnerability" + + def test_identifier_extraction_with_cve(self): + """Test CVE identifiers are correctly extracted""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + test_issue = Issue( + pkg_name="vulnerable-pkg", + pkg_version="2.0.0", + type="vulnerability", + severity="critical", + title="Known CVE", + props={"cve": ["CVE-2024-5678", "CVE-2024-9012"]}, + pkg_type="npm", + key="test-key", + purl="pkg:npm/vulnerable-pkg@2.0.0" + ) + diff.new_alerts = [test_issue] + + report = Messages.create_security_comment_gitlab(diff) + vuln = report["vulnerabilities"][0] + + # Should have socket_alert identifier + 2 CVE identifiers + assert len(vuln["identifiers"]) >= 3 + cve_identifiers = [i for i in vuln["identifiers"] if i["type"] == "cve"] + assert len(cve_identifiers) == 2 + assert any(i["value"] == "CVE-2024-5678" for i in cve_identifiers) + assert any(i["value"] == "CVE-2024-9012" for i in cve_identifiers) + + def test_identifier_extraction_with_single_cve_string(self): + """Test single CVE identifier as string""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + test_issue = Issue( + pkg_name="vulnerable-pkg", + pkg_version="2.0.0", + type="vulnerability", + severity="high", + title="Single CVE", + props={"cve": "CVE-2024-1111"}, + pkg_type="npm", + key="test-key", + purl="pkg:npm/vulnerable-pkg@2.0.0" + ) + diff.new_alerts = [test_issue] + + report = Messages.create_security_comment_gitlab(diff) + vuln = report["vulnerabilities"][0] + + cve_identifiers = [i for i in vuln["identifiers"] if i["type"] == "cve"] + assert len(cve_identifiers) == 1 + assert cve_identifiers[0]["value"] == "CVE-2024-1111" + + def test_dependency_chain_handling_transitive(self): + """Test transitive dependency path is captured""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + test_issue = Issue( + pkg_name="transitive-dep", + pkg_version="1.5.0", + type="supply-chain-risk", + severity="medium", + title="Supply Chain Risk", + introduced_by=[ + ["top-level > intermediate > transitive-dep", "package.json"] + ], + pkg_type="npm", + key="test-key", + purl="pkg:npm/transitive-dep@1.5.0" + ) + diff.new_alerts = [test_issue] + + report = Messages.create_security_comment_gitlab(diff) + vuln = report["vulnerabilities"][0] + + assert vuln["location"]["file"] == "package.json" + assert vuln["location"]["dependency"]["direct"] is False + + def test_dependency_chain_handling_direct(self): + """Test direct dependency is correctly identified""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + test_issue = Issue( + pkg_name="direct-dep", + pkg_version="3.0.0", + type="malware", + severity="critical", + title="Malware Found", + introduced_by=[ + ["direct-dep", "package.json"] + ], + pkg_type="npm", + key="test-key", + purl="pkg:npm/direct-dep@3.0.0" + ) + diff.new_alerts = [test_issue] + + report = Messages.create_security_comment_gitlab(diff) + vuln = report["vulnerabilities"][0] + + assert vuln["location"]["dependency"]["direct"] is True + + def test_severity_mapping(self): + """Test all Socket severities map to GitLab severities""" + severity_tests = [ + ("critical", "Critical"), + ("high", "High"), + ("medium", "Medium"), + ("middle", "Medium"), # Old format + ("low", "Low"), + ("unknown", "Unknown") + ] + + for socket_sev, gitlab_sev in severity_tests: + result = Messages.map_socket_severity_to_gitlab(socket_sev) + assert result == gitlab_sev, f"Failed for severity: {socket_sev}" + + def test_empty_alerts(self): + """Test report with no vulnerabilities""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + diff.new_alerts = [] + + report = Messages.create_security_comment_gitlab(diff) + + assert len(report["vulnerabilities"]) == 0 + assert report["scan"]["status"] == "success" + + def test_multiple_manifest_files(self): + """Test handling of multiple manifest files (semicolon-separated)""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + test_issue = Issue( + pkg_name="multi-manifest-pkg", + pkg_version="1.0.0", + type="supply-chain-risk", + severity="high", + title="Multiple Manifests", + introduced_by=[ + ["multi-manifest-pkg", "package.json;package-lock.json"] + ], + pkg_type="npm", + key="test-key", + purl="pkg:npm/multi-manifest-pkg@1.0.0" + ) + diff.new_alerts = [test_issue] + + report = Messages.create_security_comment_gitlab(diff) + vuln = report["vulnerabilities"][0] + + # Should use first manifest file + assert vuln["location"]["file"] == "package.json" + + def test_solution_field_included(self): + """Test solution field is included when suggestion is present""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + test_issue = Issue( + pkg_name="fixable-pkg", + pkg_version="1.0.0", + type="vulnerability", + severity="high", + title="Fixable Issue", + suggestion="Update to version 2.0.0", + pkg_type="npm", + key="test-key", + purl="pkg:npm/fixable-pkg@1.0.0" + ) + diff.new_alerts = [test_issue] + + report = Messages.create_security_comment_gitlab(diff) + vuln = report["vulnerabilities"][0] + + assert "solution" in vuln + assert vuln["solution"] == "Update to version 2.0.0" + + def test_solution_field_omitted_when_no_suggestion(self): + """Test solution field is omitted when no suggestion""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + test_issue = Issue( + pkg_name="unfixable-pkg", + pkg_version="1.0.0", + type="vulnerability", + severity="high", + title="No Fix Available", + pkg_type="npm", + key="test-key", + purl="pkg:npm/unfixable-pkg@1.0.0" + ) + diff.new_alerts = [test_issue] + + report = Messages.create_security_comment_gitlab(diff) + vuln = report["vulnerabilities"][0] + + assert "solution" not in vuln + + def test_uuid_generation_is_deterministic(self): + """Test UUID generation is deterministic for same vulnerability""" + test_issue = Issue( + pkg_name="test-pkg", + pkg_version="1.0.0", + type="malware", + severity="high", + title="Test", + pkg_type="npm", + key="test-key", + purl="pkg:npm/test-pkg@1.0.0" + ) + + uuid1 = Messages.generate_uuid_from_alert_gitlab(test_issue) + uuid2 = Messages.generate_uuid_from_alert_gitlab(test_issue) + + assert uuid1 == uuid2 + + def test_uuid_generation_differs_for_different_vulnerabilities(self): + """Test UUID generation differs for different vulnerabilities""" + issue1 = Issue( + pkg_name="test-pkg", + pkg_version="1.0.0", + type="malware", + severity="high", + title="Test", + pkg_type="npm", + key="test-key", + purl="pkg:npm/test-pkg@1.0.0" + ) + + issue2 = Issue( + pkg_name="test-pkg", + pkg_version="1.0.0", + type="vulnerability", + severity="high", + title="Test", + pkg_type="npm", + key="test-key", + purl="pkg:npm/test-pkg@1.0.0" + ) + + uuid1 = Messages.generate_uuid_from_alert_gitlab(issue1) + uuid2 = Messages.generate_uuid_from_alert_gitlab(issue2) + + assert uuid1 != uuid2 + + def test_missing_title_falls_back_to_type(self): + """Test vulnerability name falls back to type when title missing""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + test_issue = Issue( + pkg_name="no-title-pkg", + pkg_version="1.0.0", + type="malware", + severity="high", + pkg_type="npm", + key="test-key", + purl="pkg:npm/no-title-pkg@1.0.0" + ) + diff.new_alerts = [test_issue] + + report = Messages.create_security_comment_gitlab(diff) + vuln = report["vulnerabilities"][0] + + assert "malware" in vuln["name"].lower() + assert "no-title-pkg" in vuln["name"] + + def test_links_array_includes_socket_url(self): + """Test links array includes Socket.dev URL""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + test_issue = Issue( + pkg_name="linked-pkg", + pkg_version="1.0.0", + type="malware", + severity="high", + title="Test", + url="https://socket.dev/npm/package/linked-pkg", + pkg_type="npm", + key="test-key", + purl="pkg:npm/linked-pkg@1.0.0" + ) + diff.new_alerts = [test_issue] + + report = Messages.create_security_comment_gitlab(diff) + vuln = report["vulnerabilities"][0] + + assert len(vuln["links"]) == 1 + assert vuln["links"][0]["url"] == "https://socket.dev/npm/package/linked-pkg" + + def test_manifests_attribute_fallback(self): + """Test location extraction falls back to manifests attribute""" + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + + test_issue = Issue( + pkg_name="manifest-fallback-pkg", + pkg_version="1.0.0", + type="malware", + severity="high", + title="Test", + manifests="requirements.txt", + pkg_type="pypi", + key="test-key", + purl="pkg:pypi/manifest-fallback-pkg@1.0.0" + ) + diff.new_alerts = [test_issue] + + report = Messages.create_security_comment_gitlab(diff) + vuln = report["vulnerabilities"][0] + + assert vuln["location"]["file"] == "requirements.txt" diff --git a/tests/unit/test_output.py b/tests/unit/test_output.py index 35a0c4f..c47920e 100644 --- a/tests/unit/test_output.py +++ b/tests/unit/test_output.py @@ -1,7 +1,10 @@ import pytest from socketsecurity.output import OutputHandler from socketsecurity.core.classes import Diff, Issue +from socketsecurity.config import CliConfig import json +from unittest.mock import Mock +from pathlib import Path class TestOutputHandler: @pytest.fixture @@ -51,4 +54,161 @@ def test_sbom_file_saving(self, handler, tmp_path): diff.sbom = {"test": "data"} sbom_path = tmp_path / "test.json" handler.save_sbom_file(diff, str(sbom_path)) - assert sbom_path.exists() \ No newline at end of file + assert sbom_path.exists() + + def test_gitlab_security_output_enabled(self, tmp_path): + """Test GitLab security report is generated when flag enabled""" + config = CliConfig( + api_token="test", + repo="test/repo", + enable_gitlab_security=True, + gitlab_security_file=str(tmp_path / "test-report.json") + ) + + mock_sdk = Mock() + handler = OutputHandler(config=config, sdk=mock_sdk) + + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + test_issue = Issue( + pkg_name="test-pkg", + pkg_version="1.0.0", + severity="high", + title="Test", + type="malware", + manifests="package.json", + pkg_type="npm", + key="test-key", + purl="pkg:npm/test-pkg@1.0.0" + ) + diff.new_alerts = [test_issue] + + handler.handle_output(diff) + + # Verify file was created + report_path = tmp_path / "test-report.json" + assert report_path.exists() + + # Verify content structure + with open(report_path) as f: + report = json.load(f) + + assert report["scan"]["type"] == "dependency_scanning" + assert len(report["vulnerabilities"]) == 1 + assert report["vulnerabilities"][0]["name"] == "Test" + assert report["vulnerabilities"][0]["severity"] == "High" + + def test_multiple_formats_simultaneously(self, tmp_path, capsys): + """Test multiple output formats can be enabled together""" + config = CliConfig( + api_token="test", + repo="test/repo", + enable_json=True, + enable_gitlab_security=True, + gitlab_security_file=str(tmp_path / "gitlab.json") + ) + + mock_sdk = Mock() + handler = OutputHandler(config=config, sdk=mock_sdk) + + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + diff.sbom = {"test": "sbom"} + test_issue = Issue( + pkg_name="test-pkg", + pkg_version="1.0.0", + severity="high", + title="Test Issue", + type="malware", + pkg_type="npm", + key="test-key", + purl="pkg:npm/test-pkg@1.0.0" + ) + diff.new_alerts = [test_issue] + + handler.handle_output(diff) + + # JSON should be output to console (captured) + captured = capsys.readouterr() + assert len(captured.out) > 0 + + # GitLab file should exist + gitlab_path = tmp_path / "gitlab.json" + assert gitlab_path.exists() + + # Verify GitLab content + with open(gitlab_path) as f: + gitlab_report = json.load(f) + assert gitlab_report["scan"]["type"] == "dependency_scanning" + + def test_gitlab_security_file_default_path(self, tmp_path, monkeypatch): + """Test GitLab security report uses default filename""" + # Change to tmp directory for this test + monkeypatch.chdir(tmp_path) + + config = CliConfig( + api_token="test", + repo="test/repo", + enable_gitlab_security=True + ) + + mock_sdk = Mock() + handler = OutputHandler(config=config, sdk=mock_sdk) + + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + diff.new_alerts = [] + + handler.handle_output(diff) + + # Should create file with default name + default_path = tmp_path / "gl-dependency-scanning-report.json" + assert default_path.exists() + + def test_gitlab_output_skipped_when_no_diff(self): + """Test GitLab output is skipped when NO_DIFF_RAN""" + config = CliConfig( + api_token="test", + repo="test/repo", + enable_gitlab_security=True, + gitlab_security_file="test-report.json" + ) + + mock_sdk = Mock() + handler = OutputHandler(config=config, sdk=mock_sdk) + + diff = Diff() + diff.id = "NO_DIFF_RAN" + diff.new_alerts = [] + + handler.handle_output(diff) + + # File should not be created + assert not Path("test-report.json").exists() + + def test_gitlab_security_creates_parent_directories(self, tmp_path): + """Test GitLab security file creation creates parent directories""" + config = CliConfig( + api_token="test", + repo="test/repo", + enable_gitlab_security=True, + gitlab_security_file=str(tmp_path / "reports" / "security" / "gitlab.json") + ) + + mock_sdk = Mock() + handler = OutputHandler(config=config, sdk=mock_sdk) + + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + diff.new_alerts = [] + + handler.handle_output(diff) + + # Verify parent directories were created + report_path = tmp_path / "reports" / "security" / "gitlab.json" + assert report_path.exists() + assert report_path.parent.exists() \ No newline at end of file From a389972da437acd820a60288412faf00a2c1dbdc Mon Sep 17 00:00:00 2001 From: Jonathan Mucha Date: Mon, 12 Jan 2026 11:01:40 -0500 Subject: [PATCH 2/2] capturing all recent changes --- .gitlab-ci-test.yml | 113 ++++++++++++++ GITLAB_TESTING_GUIDE.md | 294 +++++++++++++++++++++++++++++++++++++ test-gitlab-integration.sh | 135 +++++++++++++++++ uv.lock | 2 +- 4 files changed, 543 insertions(+), 1 deletion(-) create mode 100644 .gitlab-ci-test.yml create mode 100644 GITLAB_TESTING_GUIDE.md create mode 100755 test-gitlab-integration.sh diff --git a/.gitlab-ci-test.yml b/.gitlab-ci-test.yml new file mode 100644 index 0000000..6b5cfcf --- /dev/null +++ b/.gitlab-ci-test.yml @@ -0,0 +1,113 @@ +# Test GitLab CI configuration for Socket Security Dashboard integration +# Rename to .gitlab-ci.yml to activate, or create a test pipeline in GitLab UI + +stages: + - test + - security + +# Test 1: Install from branch and generate report +socket_security_test: + stage: security + image: python:3.11 + before_script: + - pip install --upgrade pip + # Install directly from the git branch + - pip install git+https://github.com/SocketDev/socket-python-cli.git@mucha-dev-gitlab-security-output + script: + - echo "Testing GitLab Security Dashboard integration..." + - socketcli --version + - socketcli --help | grep "gitlab-security" + - | + socketcli \ + --api-token $SOCKET_API_TOKEN \ + --repo socket-python-cli \ + --target-path . \ + --enable-gitlab-security \ + --gitlab-security-file gl-dependency-scanning-report.json + - echo "Verifying report was generated..." + - ls -lh gl-dependency-scanning-report.json + - echo "Report contents preview:" + - cat gl-dependency-scanning-report.json | head -50 + artifacts: + reports: + dependency_scanning: gl-dependency-scanning-report.json + paths: + - gl-dependency-scanning-report.json + expire_in: 1 week + only: + - branches + allow_failure: false + +# Test 2: Validate report schema +validate_gitlab_report: + stage: test + image: python:3.11 + dependencies: + - socket_security_test + script: + - echo "Validating GitLab Security report structure..." + - | + python3 << 'VALIDATE' + import json + import sys + + with open('gl-dependency-scanning-report.json') as f: + report = json.load(f) + + # Validate required fields + assert 'version' in report, "Missing 'version' field" + assert 'scan' in report, "Missing 'scan' field" + assert 'vulnerabilities' in report, "Missing 'vulnerabilities' field" + + # Validate scan structure + scan = report['scan'] + assert scan['type'] == 'dependency_scanning', f"Invalid scan type: {scan['type']}" + assert 'analyzer' in scan, "Missing 'analyzer' in scan" + assert 'scanner' in scan, "Missing 'scanner' in scan" + assert scan['analyzer']['id'] == 'socket-security', "Invalid analyzer ID" + assert scan['scanner']['id'] == 'socket-cli', "Invalid scanner ID" + + print(f"✓ Report structure is valid") + print(f"✓ Schema version: {report['version']}") + print(f"✓ Scan type: {scan['type']}") + print(f"✓ Vulnerabilities found: {len(report['vulnerabilities'])}") + + if report['vulnerabilities']: + print(f"\nFirst 3 vulnerabilities:") + for i, vuln in enumerate(report['vulnerabilities'][:3], 1): + print(f" {i}. {vuln['severity']}: {vuln['name']}") + print(f" Package: {vuln['location']['dependency']['package']['name']}@{vuln['location']['dependency']['version']}") + + print("\n✅ GitLab report validation successful!") + VALIDATE + only: + - branches + +# Test 3: Multiple formats simultaneously +test_multiple_formats: + stage: security + image: python:3.11 + before_script: + - pip install git+https://github.com/SocketDev/socket-python-cli.git@mucha-dev-gitlab-security-output + script: + - echo "Testing multiple output formats..." + - | + socketcli \ + --api-token $SOCKET_API_TOKEN \ + --repo socket-python-cli \ + --target-path . \ + --enable-json \ + --enable-gitlab-security \ + --gitlab-security-file reports/gitlab-security.json > json-output.txt 2>&1 + - echo "Verifying both formats were generated..." + - ls -lh reports/gitlab-security.json + - grep -q "vulnerabilities" reports/gitlab-security.json && echo "✓ GitLab report contains vulnerabilities field" + - grep -q "scan_failed" json-output.txt && echo "✓ JSON output was generated" + artifacts: + paths: + - reports/gitlab-security.json + - json-output.txt + expire_in: 1 day + only: + - branches + allow_failure: true diff --git a/GITLAB_TESTING_GUIDE.md b/GITLAB_TESTING_GUIDE.md new file mode 100644 index 0000000..943740e --- /dev/null +++ b/GITLAB_TESTING_GUIDE.md @@ -0,0 +1,294 @@ +# GitLab Security Dashboard Integration - Testing Guide + +This guide explains how to test the GitLab Security Dashboard integration before merging to production. + +## Prerequisites + +- GitLab account with access to create pipelines +- Socket API token (`SOCKET_API_TOKEN`) +- Access to a GitLab repository (can be a fork or test repo) + +## Testing Approach Options + +### Option A: Test in GitLab.com (Quickest) + +1. **Push branch to GitLab**: + ```bash + # Add GitLab remote if not already added + git remote add gitlab git@gitlab.com:your-username/socket-python-cli.git + + # Push the feature branch + git push gitlab mucha-dev-gitlab-security-output + ``` + +2. **Configure CI/CD Variables**: + - Go to: Settings → CI/CD → Variables + - Add variable: `SOCKET_API_TOKEN` (masked, not protected) + +3. **Create test pipeline**: + ```bash + # Rename the test config + mv .gitlab-ci-test.yml .gitlab-ci.yml + git add .gitlab-ci.yml + git commit -m "test: add GitLab CI configuration for testing" + git push gitlab mucha-dev-gitlab-security-output + ``` + +4. **Monitor the pipeline**: + - Go to: CI/CD → Pipelines + - Click on the running pipeline + - Watch the `socket_security_test` job + +5. **Verify Security Dashboard**: + - Go to: Security & Compliance → Vulnerability Report + - Check if Socket vulnerabilities appear + - Or go to a Merge Request → Security tab + +### Option B: Test with GitLab Runner Locally + +Install GitLab Runner on your machine: + +```bash +# macOS +brew install gitlab-runner + +# Start runner +gitlab-runner exec docker socket_security_test \ + --docker-image python:3.11 \ + --env SOCKET_API_TOKEN=$SOCKET_API_TOKEN +``` + +### Option C: Create a Test Project in GitLab + +1. **Create a new test repository in GitLab** +2. **Add Socket CLI as dependency**: + ```yaml + # .gitlab-ci.yml + socket_test: + stage: test + image: python:3.11 + before_script: + - pip install git+https://github.com/SocketDev/socket-python-cli.git@mucha-dev-gitlab-security-output + script: + - socketcli --enable-gitlab-security --repo test-repo --target-path . + artifacts: + reports: + dependency_scanning: gl-dependency-scanning-report.json + ``` + +3. **Add a test manifest file** (e.g., `package.json` or `requirements.txt`) +4. **Push and run pipeline** + +## What to Verify + +### 1. Pipeline Success ✅ + +- [ ] Pipeline completes without errors +- [ ] `socket_security_test` job succeeds +- [ ] Artifacts are uploaded successfully + +### 2. Report File Generation ✅ + +- [ ] `gl-dependency-scanning-report.json` is created +- [ ] File size is reasonable (not empty, not huge) +- [ ] JSON is valid (can be parsed) + +### 3. Report Schema Validation ✅ + +Download and inspect the artifact: + +```bash +# Download from GitLab UI: Job → Browse → Download +# Or use GitLab API +curl --header "PRIVATE-TOKEN: " \ + "https://gitlab.com/api/v4/projects//jobs//artifacts/gl-dependency-scanning-report.json" \ + > report.json + +# Validate structure +cat report.json | python3 -m json.tool + +# Check required fields +cat report.json | jq 'keys' +# Should include: version, scan, vulnerabilities + +# Check scan metadata +cat report.json | jq '.scan' +# Should show: analyzer, scanner, type, status + +# Check vulnerabilities +cat report.json | jq '.vulnerabilities | length' +# Shows count of vulnerabilities + +cat report.json | jq '.vulnerabilities[0]' +# Shows first vulnerability structure +``` + +### 4. Security Dashboard Integration ✅ + +**In GitLab UI:** + +- [ ] Go to: Security & Compliance → Vulnerability Report +- [ ] Vulnerabilities from Socket appear in the list +- [ ] Severity levels display correctly (Critical, High, Medium, Low) +- [ ] Package names and versions are shown +- [ ] CVE identifiers link correctly + +**In Merge Request:** + +- [ ] Create a test MR from your branch +- [ ] Go to MR → Security tab +- [ ] Socket findings appear in the security widget +- [ ] Can expand to see vulnerability details + +### 5. Multiple Format Testing ✅ + +Test that multiple formats work simultaneously: + +```bash +socketcli \ + --enable-json \ + --enable-gitlab-security \ + --repo test-repo \ + --target-path . + +# Verify both outputs: +ls -lh gl-dependency-scanning-report.json +# JSON should also be in stdout +``` + +## Validation Checklist + +### Report Structure +- [ ] `version` field is "15.0.0" +- [ ] `scan.type` is "dependency_scanning" +- [ ] `scan.analyzer.id` is "socket-security" +- [ ] `scan.scanner.id` is "socket-cli" +- [ ] `scan.status` is "success" +- [ ] `vulnerabilities` is an array (can be empty) + +### Vulnerability Objects (if any found) +- [ ] Each has `id`, `category`, `name`, `severity`, `message` +- [ ] Each has `identifiers` array with at least socket_alert type +- [ ] CVE identifiers included (if applicable) +- [ ] Each has `location` with `file` and `dependency` fields +- [ ] `location.dependency.direct` is boolean +- [ ] `location.dependency.package.name` is present +- [ ] `location.dependency.version` is present +- [ ] `links` array includes Socket.dev URL + +### Alert Filtering +- [ ] Only error/warn level alerts are included +- [ ] Ignored alerts are excluded +- [ ] Monitor-only alerts are excluded + +## Troubleshooting + +### Issue: Report not appearing in Security Dashboard + +**Check:** +1. Artifact is uploaded: CI/CD → Jobs → Browse artifacts +2. Artifact path matches: `reports.dependency_scanning: gl-dependency-scanning-report.json` +3. Job succeeded (failed jobs don't register reports) +4. GitLab version supports Dependency Scanning (12.0+) + +**Solution:** +```yaml +artifacts: + reports: + dependency_scanning: gl-dependency-scanning-report.json + # Must be at job level, not global +``` + +### Issue: Empty vulnerabilities array + +**This is normal if:** +- No new security issues detected +- All alerts are at monitor level (not error/warn) +- All alerts are ignored by policy + +**Verify:** +- Check Socket.dev dashboard for actual findings +- Review Socket policy configuration + +### Issue: Schema validation errors + +**Common causes:** +- Missing required fields +- Invalid severity values +- Malformed JSON + +**Debug:** +```bash +# Validate JSON syntax +cat gl-dependency-scanning-report.json | python3 -m json.tool + +# Check against GitLab schema +# Download schema from: +# https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/master/dist/dependency-scanning-report-format.json +``` + +### Issue: Installation fails in pipeline + +**Solution:** +```yaml +before_script: + - pip install --upgrade pip + - pip install git+https://github.com/SocketDev/socket-python-cli.git@mucha-dev-gitlab-security-output + # Or install from local wheel if testing locally built package +``` + +## Production Deployment Checklist + +Before merging to main: + +- [ ] All tests pass in GitLab CI +- [ ] Report appears in Security Dashboard +- [ ] Vulnerabilities display with correct severity +- [ ] CVE links work correctly +- [ ] Multiple format support works +- [ ] Documentation is complete and accurate +- [ ] Unit tests pass (`pytest tests/unit/test_gitlab_format.py`) +- [ ] Integration tests pass +- [ ] Code review completed +- [ ] No breaking changes to existing functionality + +## Example Test Results + +### Successful Pipeline Output: +``` +$ socketcli --enable-gitlab-security --repo test/repo +2026-01-12 12:00:00,000: Starting Socket Security CLI version 2.2.63 +2026-01-12 12:00:02,000: Full scan created with ID: abc-123 +2026-01-12 12:00:02,000: GitLab Security report saved to gl-dependency-scanning-report.json +✓ Job succeeded +``` + +### Valid Report Structure: +```json +{ + "version": "15.0.0", + "scan": { + "analyzer": {"id": "socket-security", "name": "Socket Security"}, + "scanner": {"id": "socket-cli", "name": "Socket CLI"}, + "type": "dependency_scanning", + "status": "success" + }, + "vulnerabilities": [...] +} +``` + +## Resources + +- [GitLab Dependency Scanning Documentation](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/) +- [GitLab Security Report Schemas](https://gitlab.com/gitlab-org/security-products/security-report-schemas) +- [Socket GitLab Integration Docs](https://docs.socket.dev/docs/gitlab) +- [Socket CLI Documentation](./README.md#gitlab-security-dashboard-integration) + +## Support + +If you encounter issues: +1. Check the troubleshooting section above +2. Review GitLab CI job logs +3. Validate report structure +4. Check Socket.dev dashboard for actual findings +5. Open an issue with pipeline logs and report file diff --git a/test-gitlab-integration.sh b/test-gitlab-integration.sh new file mode 100755 index 0000000..4f86079 --- /dev/null +++ b/test-gitlab-integration.sh @@ -0,0 +1,135 @@ +#!/bin/bash +# Quick test script for GitLab Security Dashboard integration +# Usage: ./test-gitlab-integration.sh + +set -e + +echo "🧪 Testing GitLab Security Dashboard Integration" +echo "================================================" +echo "" + +# Check if in virtual environment +if [[ -z "$VIRTUAL_ENV" ]]; then + echo "⚠️ Virtual environment not activated" + echo " Activating .venv..." + source .venv/bin/activate +fi + +# Test 1: Verify CLI flags exist +echo "✅ Test 1: Checking CLI flags..." +python -m socketsecurity.socketcli --help | grep -q "gitlab-security" && \ + echo " ✓ --enable-gitlab-security flag found" || \ + (echo " ✗ Flag not found" && exit 1) + +python -m socketsecurity.socketcli --help | grep -q "gitlab-security-file" && \ + echo " ✓ --gitlab-security-file flag found" || \ + (echo " ✗ Flag not found" && exit 1) + +echo "" + +# Test 2: Generate test report +echo "✅ Test 2: Generating GitLab Security report..." +if [[ -z "$SOCKET_API_TOKEN" ]]; then + echo " ⚠️ SOCKET_API_TOKEN not set. Using test mode." + echo " Set SOCKET_API_TOKEN to test with real API" +else + python -m socketsecurity.socketcli \ + --enable-gitlab-security \ + --gitlab-security-file test-gitlab-report.json \ + --repo socket-python-cli \ + --target-path . 2>&1 | head -20 + + if [[ -f "test-gitlab-report.json" ]]; then + echo " ✓ Report file created" + else + echo " ⚠️ Report file not created (may be expected without API token)" + fi +fi + +echo "" + +# Test 3: Validate report structure (if file exists) +if [[ -f "test-gitlab-report.json" ]]; then + echo "✅ Test 3: Validating report structure..." + + python3 << 'VALIDATE' +import json +import sys + +try: + with open('test-gitlab-report.json') as f: + report = json.load(f) + + # Check required fields + assert 'version' in report, "Missing version" + assert 'scan' in report, "Missing scan" + assert 'vulnerabilities' in report, "Missing vulnerabilities" + assert report['scan']['type'] == 'dependency_scanning', "Invalid scan type" + + print(f" ✓ Valid GitLab report structure") + print(f" ✓ Schema version: {report['version']}") + print(f" ✓ Vulnerabilities: {len(report['vulnerabilities'])}") + + if report['vulnerabilities']: + print(f"\n Sample vulnerability:") + vuln = report['vulnerabilities'][0] + print(f" - {vuln['severity']}: {vuln['name']}") + print(f" - Package: {vuln['location']['dependency']['package']['name']}") + + sys.exit(0) +except Exception as e: + print(f" ✗ Validation failed: {e}") + sys.exit(1) +VALIDATE + + if [[ $? -eq 0 ]]; then + echo " ✓ Report validation passed" + else + echo " ✗ Report validation failed" + exit 1 + fi +else + echo "⏭️ Test 3: Skipped (no report file generated)" +fi + +echo "" + +# Test 4: Multiple format support +echo "✅ Test 4: Testing multiple format support..." +echo ' Testing --enable-json --enable-gitlab-security...' + +if [[ ! -z "$SOCKET_API_TOKEN" ]]; then + python -m socketsecurity.socketcli \ + --enable-json \ + --enable-gitlab-security \ + --gitlab-security-file test-multi-format.json \ + --repo socket-python-cli \ + --target-path . 2>&1 | grep -q '"scan_failed"' && \ + echo " ✓ JSON output detected" || \ + echo " ⚠️ JSON output not detected" + + if [[ -f "test-multi-format.json" ]]; then + echo " ✓ GitLab report generated alongside JSON" + fi +else + echo " ⏭️ Skipped (requires SOCKET_API_TOKEN)" +fi + +echo "" +echo "================================================" +echo "🎉 Local testing complete!" +echo "" +echo "Next steps:" +echo " 1. Review test-gitlab-report.json (if generated)" +echo " 2. Push branch to GitLab for pipeline testing" +echo " 3. See GITLAB_TESTING_GUIDE.md for full test plan" +echo "" +echo "To test in GitLab CI:" +echo " git push gitlab mucha-dev-gitlab-security-output" +echo " # Then check CI/CD → Pipelines in GitLab" + +# Cleanup +if [[ -f "test-gitlab-report.json" ]]; then + echo "" + echo "📄 Test report location: ./test-gitlab-report.json" +fi diff --git a/uv.lock b/uv.lock index f2b7477..456ec72 100644 --- a/uv.lock +++ b/uv.lock @@ -1263,7 +1263,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.2.62" +version = "2.2.63" source = { editable = "." } dependencies = [ { name = "bs4" },