From 1a4bdf17e344aeb72849a6fceac4ab35a7c56831 Mon Sep 17 00:00:00 2001 From: David Larsen Date: Fri, 27 Feb 2026 15:39:22 -0500 Subject: [PATCH 1/4] Add structured findings to webhook payload Webhook payloads now include a machine-readable findings array alongside the existing markdown content. Each connector (Trivy, Socket Tier1, OpenGrep, TruffleHog) builds structured finding objects during format. The WebhookNotifier aggregates these into the payload with a severity summary and ISO 8601 timestamp. The notification.content field remains for backward compatibility. TruffleHog findings intentionally omit redacted secret values from the structured output. --- .../core/connector/opengrep/webhook.py | 38 +++- .../core/connector/socket_tier1/webhook.py | 20 +- socket_basics/core/connector/trivy/webhook.py | 32 ++- .../core/connector/trufflehog/webhook.py | 18 +- .../core/notification/webhook_notifier.py | 38 +++- tests/test_webhook_notifier_params.py | 113 +++++++++- tests/test_webhook_payload.py | 206 ++++++++++++++++++ 7 files changed, 429 insertions(+), 36 deletions(-) create mode 100644 tests/test_webhook_payload.py diff --git a/socket_basics/core/connector/opengrep/webhook.py b/socket_basics/core/connector/opengrep/webhook.py index 8ba5d25..d66caea 100644 --- a/socket_basics/core/connector/opengrep/webhook.py +++ b/socket_basics/core/connector/opengrep/webhook.py @@ -35,30 +35,45 @@ def format_notifications(groups: Dict[str, List[Dict[str, Any]]]) -> List[Dict[s for subtype, items in groups.items(): if not items: # Skip empty groups continue - + rows = [] + findings: List[Dict[str, Any]] = [] for item in items: c = item['component'] a = item['alert'] props = a.get('props', {}) or {} full_path = props.get('filePath', a.get('location', {}).get('path')) or '-' - + try: file_name = Path(full_path).name except Exception: file_name = full_path - + + rule = props.get('ruleId', a.get('title', '')) + severity = a.get('severity', '') + lines = f"{props.get('startLine','')}-{props.get('endLine','')}" + rows.append([ - props.get('ruleId', a.get('title', '')), - a.get('severity', ''), + rule, + severity, file_name, full_path, - f"{props.get('startLine','')}-{props.get('endLine','')}", + lines, props.get('codeSnippet', '') or '', subtype, 'opengrep' ]) - + + findings.append({ + 'rule': rule, + 'severity': severity, + 'file': file_name, + 'path': full_path, + 'lines': lines, + 'language': subtype, + 'scanner': 'opengrep', + }) + # Create a separate dataset for each subtype/language group display_name = subtype_names.get(subtype, subtype.upper()) headers = ['Rule', 'Severity', 'File', 'Path', 'Lines', 'Code', 'SubType', 'Scanner'] @@ -67,13 +82,14 @@ def format_notifications(groups: Dict[str, List[Dict[str, Any]]]) -> List[Dict[s content_rows = [] for row in rows: content_rows.append(' | '.join(str(cell) for cell in row)) - + content = '\n'.join([header_row, separator_row] + content_rows) if rows else f"No {display_name} issues found." - + tables.append({ 'title': display_name, - 'content': content + 'content': content, + 'findings': findings, }) - + # Return list of tables - one per language group return tables \ No newline at end of file diff --git a/socket_basics/core/connector/socket_tier1/webhook.py b/socket_basics/core/connector/socket_tier1/webhook.py index df5ed20..92c5b85 100644 --- a/socket_basics/core/connector/socket_tier1/webhook.py +++ b/socket_basics/core/connector/socket_tier1/webhook.py @@ -24,18 +24,19 @@ def _make_purl(comp: Dict[str, Any]) -> str: def format_notifications(components_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """Format for generic webhook - flexible structured format.""" + """Format for generic webhook - flexible structured format with findings.""" rows = [] + findings: List[Dict[str, Any]] = [] for comp in components_list: comp_name = str(comp.get('name') or comp.get('id') or '-') - + for a in comp.get('alerts', []): props = a.get('props', {}) or {} purl = str(props.get('purl') or _make_purl(comp) or comp_name) cve_id = str(props.get('ghsaId') or props.get('cveId') or a.get('title') or '') severity = str(a.get('severity') or props.get('severity') or '') reachability = str(props.get('reachability') or '') - + rows.append([ cve_id, severity, @@ -46,6 +47,16 @@ def format_notifications(components_list: List[Dict[str, Any]]) -> List[Dict[str str(props.get('ghsaId', '')), 'socket-tier1' ]) + + findings.append({ + 'package': comp_name, + 'version': str(comp.get('version', '')), + 'purl': purl, + 'cve': cve_id, + 'severity': severity, + 'reachability': reachability, + 'scanner': 'socket-tier1', + }) # Format as structured data for webhook if not rows: @@ -62,5 +73,6 @@ def format_notifications(components_list: List[Dict[str, Any]]) -> List[Dict[str return [{ 'title': 'Socket Tier1 Reachability Analysis', - 'content': content + 'content': content, + 'findings': findings, }] \ No newline at end of file diff --git a/socket_basics/core/connector/trivy/webhook.py b/socket_basics/core/connector/trivy/webhook.py index d71eb0e..2a691fb 100644 --- a/socket_basics/core/connector/trivy/webhook.py +++ b/socket_basics/core/connector/trivy/webhook.py @@ -18,7 +18,8 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc """ # Group vulnerabilities by package and severity package_groups = defaultdict(lambda: defaultdict(set)) # Use set to avoid duplicates - + findings: List[Dict[str, Any]] = [] + if scan_type == 'dockerfile': # Process dockerfile components for comp in mapping.values(): @@ -28,27 +29,45 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc severity = str(alert.get('severity', '')) message = str(alert.get('description', '')) resolution = str(props.get('resolution', '')) - + rule_info = f"{rule_id}|{message}|{resolution}" package_groups[rule_id][severity].add(rule_info) - + + findings.append({ + 'rule': rule_id, + 'severity': severity, + 'message': message, + 'resolution': resolution, + 'scanner': 'trivy', + }) + else: # image or vuln # Process package vulnerability components for comp in mapping.values(): comp_name = str(comp.get('name') or comp.get('id') or '-') comp_version = str(comp.get('version', '')) ecosystem = comp.get('qualifiers', {}).get('ecosystem', 'unknown') - + if comp_version: package_key = f"pkg:{ecosystem}/{comp_name}@{comp_version}" else: package_key = f"pkg:{ecosystem}/{comp_name}" - + for alert in comp.get('alerts', []): props = alert.get('props', {}) or {} cve_id = str(props.get('vulnerabilityId', '') or alert.get('title', '')) severity = str(alert.get('severity', '')) package_groups[package_key][severity].add(cve_id) + + findings.append({ + 'package': comp_name, + 'version': comp_version, + 'ecosystem': ecosystem, + 'purl': package_key, + 'cves': [cve_id], + 'severity': severity, + 'scanner': 'trivy', + }) # Create rows with proper formatting rows = [] @@ -111,5 +130,6 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc return [{ 'title': title, - 'content': content + 'content': content, + 'findings': findings, }] \ No newline at end of file diff --git a/socket_basics/core/connector/trufflehog/webhook.py b/socket_basics/core/connector/trufflehog/webhook.py index 9bb4ffc..ae41b0b 100644 --- a/socket_basics/core/connector/trufflehog/webhook.py +++ b/socket_basics/core/connector/trufflehog/webhook.py @@ -8,8 +8,9 @@ def format_notifications(mapping: Dict[str, Any]) -> List[Dict[str, Any]]: - """Format for generic webhook - flexible structured format.""" + """Format for generic webhook - flexible structured format with findings.""" rows = [] + findings: List[Dict[str, Any]] = [] for comp in mapping.values(): for a in comp.get('alerts', []): props = a.get('props', {}) or {} @@ -20,7 +21,7 @@ def format_notifications(mapping: Dict[str, Any]) -> List[Dict[str, Any]]: redacted = str(props.get('redactedValue', '')) verified = props.get('verified', False) secret_type = str(props.get('secretType', '')) - + rows.append([ detector, severity, @@ -31,6 +32,16 @@ def format_notifications(mapping: Dict[str, Any]) -> List[Dict[str, Any]]: str(verified), 'trufflehog' ]) + + # Omit redacted_value from structured findings to avoid leaking secrets + findings.append({ + 'detector': detector, + 'severity': severity, + 'file': file_path, + 'line': line, + 'verified': verified, + 'scanner': 'trufflehog', + }) # Format as structured data if not rows: @@ -47,5 +58,6 @@ def format_notifications(mapping: Dict[str, Any]) -> List[Dict[str, Any]]: return [{ 'title': 'TruffleHog Secret Detection Results', - 'content': content + 'content': content, + 'findings': findings, }] \ No newline at end of file diff --git a/socket_basics/core/notification/webhook_notifier.py b/socket_basics/core/notification/webhook_notifier.py index a5a5279..1f65e74 100644 --- a/socket_basics/core/notification/webhook_notifier.py +++ b/socket_basics/core/notification/webhook_notifier.py @@ -1,12 +1,25 @@ -from typing import Any, Dict +from datetime import datetime, timezone +from typing import Any, Dict, List import logging +import requests + from socket_basics.core.notification.base import BaseNotifier from socket_basics.core.config import get_webhook_url logger = logging.getLogger(__name__) +def _compute_summary(findings: List[Dict[str, Any]]) -> Dict[str, int]: + """Compute severity counts from a findings list.""" + counts: Dict[str, int] = {'total': len(findings), 'critical': 0, 'high': 0, 'medium': 0, 'low': 0} + for f in findings: + sev = str(f.get('severity', '')).lower() + if sev in counts: + counts[sev] += 1 + return counts + + class WebhookNotifier(BaseNotifier): """Webhook notifier: sends security findings to HTTP webhook endpoints. @@ -47,12 +60,10 @@ def notify(self, facts: Dict[str, Any]) -> None: # Send each notification as a separate webhook for item in valid_notifications: - title = item['title'] - content = item['content'] - self._send_webhook(facts, title, content) + self._send_webhook(facts, item) - def _send_webhook(self, facts: Dict[str, Any], title: str, content: str) -> None: - """Send a single webhook with title and content.""" + def _send_webhook(self, facts: Dict[str, Any], item: Dict[str, Any]) -> None: + """Send a single webhook with structured payload.""" if not self.url: logger.warning('WebhookNotifier: no webhook URL configured') return @@ -61,25 +72,30 @@ def _send_webhook(self, facts: Dict[str, Any], title: str, content: str) -> None repo = facts.get('repository', 'Unknown') branch = facts.get('branch', 'Unknown') - # Create webhook payload with pre-formatted content + title = item['title'] + content = item['content'] + findings = item.get('findings', []) + payload = { 'repository': repo, 'branch': branch, 'scanner': 'socket-security', - 'timestamp': facts.get('timestamp'), + 'timestamp': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'), + 'scan_type': title, + 'summary': _compute_summary(findings), + 'findings': findings, 'notification': { 'title': title, - 'content': content + 'content': content, } } try: - import requests resp = requests.post(self.url, json=payload, timeout=10) if resp.status_code >= 400: logger.warning('WebhookNotifier: HTTP error %s: %s', resp.status_code, resp.text[:200]) else: logger.info('WebhookNotifier: sent webhook for "%s"', title) - + except Exception as e: logger.error('WebhookNotifier: exception sending webhook: %s', e) diff --git a/tests/test_webhook_notifier_params.py b/tests/test_webhook_notifier_params.py index 996f105..436f22b 100644 --- a/tests/test_webhook_notifier_params.py +++ b/tests/test_webhook_notifier_params.py @@ -1,6 +1,7 @@ import os +from unittest.mock import patch, MagicMock -from socket_basics.core.notification.webhook_notifier import WebhookNotifier +from socket_basics.core.notification.webhook_notifier import WebhookNotifier, _compute_summary from socket_basics.core.notification.manager import NotificationManager @@ -85,3 +86,113 @@ def test_webhook_app_config_precedence_over_env(monkeypatch): webhook = next(n for n in nm.notifiers if getattr(n, "name", "") == "webhook") assert webhook.url == "https://dashboard.example.com/hook" + + +# --- Structured payload tests --- + +def _make_notifier(): + return WebhookNotifier({"webhook_url": "https://example.com/hook"}) + + +def _make_facts(notifications): + return {'notifications': notifications} + + +def test_compute_summary_empty(): + assert _compute_summary([]) == {'total': 0, 'critical': 0, 'high': 0, 'medium': 0, 'low': 0} + + +def test_compute_summary_counts(): + findings = [ + {'severity': 'critical'}, + {'severity': 'critical'}, + {'severity': 'high'}, + {'severity': 'medium'}, + {'severity': 'low'}, + {'severity': 'low'}, + ] + result = _compute_summary(findings) + assert result == {'total': 6, 'critical': 2, 'high': 1, 'medium': 1, 'low': 2} + + +def test_compute_summary_unknown_severity_in_total(): + findings = [{'severity': 'info'}, {'severity': 'critical'}] + result = _compute_summary(findings) + assert result['total'] == 2 + assert result['critical'] == 1 + + +@patch('socket_basics.core.notification.webhook_notifier.requests') +def test_payload_has_timestamp(mock_requests): + mock_requests.post.return_value = MagicMock(status_code=200) + n = _make_notifier() + findings = [{'severity': 'high', 'package': 'foo', 'scanner': 'trivy'}] + n.notify(_make_facts([{'title': 'Test', 'content': 'table', 'findings': findings}])) + + payload = mock_requests.post.call_args[1]['json'] + assert payload['timestamp'] is not None + assert 'T' in payload['timestamp'] + assert payload['timestamp'].endswith('Z') + + +@patch('socket_basics.core.notification.webhook_notifier.requests') +def test_payload_has_summary(mock_requests): + mock_requests.post.return_value = MagicMock(status_code=200) + n = _make_notifier() + findings = [ + {'severity': 'critical', 'package': 'a'}, + {'severity': 'high', 'package': 'b'}, + {'severity': 'high', 'package': 'c'}, + ] + n.notify(_make_facts([{'title': 'Test', 'content': 'md', 'findings': findings}])) + + payload = mock_requests.post.call_args[1]['json'] + assert payload['summary']['total'] == 3 + assert payload['summary']['critical'] == 1 + assert payload['summary']['high'] == 2 + + +@patch('socket_basics.core.notification.webhook_notifier.requests') +def test_payload_has_findings_array(mock_requests): + mock_requests.post.return_value = MagicMock(status_code=200) + n = _make_notifier() + findings = [{'severity': 'low', 'package': 'x', 'scanner': 'trivy'}] + n.notify(_make_facts([{'title': 'T', 'content': 'c', 'findings': findings}])) + + payload = mock_requests.post.call_args[1]['json'] + assert isinstance(payload['findings'], list) + assert len(payload['findings']) == 1 + assert payload['findings'][0]['package'] == 'x' + + +@patch('socket_basics.core.notification.webhook_notifier.requests') +def test_backward_compat_notification_field(mock_requests): + mock_requests.post.return_value = MagicMock(status_code=200) + n = _make_notifier() + n.notify(_make_facts([{'title': 'Title', 'content': 'markdown table'}])) + + payload = mock_requests.post.call_args[1]['json'] + assert payload['notification']['title'] == 'Title' + assert payload['notification']['content'] == 'markdown table' + + +@patch('socket_basics.core.notification.webhook_notifier.requests') +def test_missing_findings_defaults_empty(mock_requests): + """Connectors that haven't been updated yet still work (no findings key).""" + mock_requests.post.return_value = MagicMock(status_code=200) + n = _make_notifier() + n.notify(_make_facts([{'title': 'T', 'content': 'c'}])) + + payload = mock_requests.post.call_args[1]['json'] + assert payload['findings'] == [] + assert payload['summary']['total'] == 0 + + +@patch('socket_basics.core.notification.webhook_notifier.requests') +def test_scan_type_set_from_title(mock_requests): + mock_requests.post.return_value = MagicMock(status_code=200) + n = _make_notifier() + n.notify(_make_facts([{'title': 'Socket CVE Scanning Results: Dockerfile', 'content': 'c'}])) + + payload = mock_requests.post.call_args[1]['json'] + assert payload['scan_type'] == 'Socket CVE Scanning Results: Dockerfile' diff --git a/tests/test_webhook_payload.py b/tests/test_webhook_payload.py new file mode 100644 index 0000000..9282dba --- /dev/null +++ b/tests/test_webhook_payload.py @@ -0,0 +1,206 @@ +"""Tests for each connector's webhook formatter returning structured findings.""" + +from socket_basics.core.connector.trivy.webhook import format_notifications as trivy_format +from socket_basics.core.connector.socket_tier1.webhook import format_notifications as tier1_format +from socket_basics.core.connector.opengrep.webhook import format_notifications as opengrep_format +from socket_basics.core.connector.trufflehog.webhook import format_notifications as trufflehog_format + + +# --- Trivy --- + +class TestTrivyWebhookFindings: + def _make_vuln_mapping(self): + return { + 'comp1': { + 'name': 'bson', + 'version': '1.0.9', + 'qualifiers': {'ecosystem': 'npm'}, + 'alerts': [{ + 'severity': 'critical', + 'props': {'vulnerabilityId': 'CVE-2020-7610'}, + }], + } + } + + def _make_dockerfile_mapping(self): + return { + 'comp1': { + 'alerts': [{ + 'severity': 'high', + 'title': 'DS001', + 'description': 'Use COPY instead of ADD', + 'props': {'ruleId': 'DS001', 'resolution': 'Replace ADD with COPY'}, + }], + } + } + + def test_vuln_findings_key_present(self): + result = trivy_format(self._make_vuln_mapping(), 'test', 'vuln') + assert 'findings' in result[0] + + def test_vuln_findings_structure(self): + result = trivy_format(self._make_vuln_mapping(), 'test', 'vuln') + f = result[0]['findings'][0] + assert f['package'] == 'bson' + assert f['version'] == '1.0.9' + assert f['ecosystem'] == 'npm' + assert f['purl'] == 'pkg:npm/bson@1.0.9' + assert f['cves'] == ['CVE-2020-7610'] + assert f['severity'] == 'critical' + assert f['scanner'] == 'trivy' + + def test_vuln_no_pkg_unknown(self): + result = trivy_format(self._make_vuln_mapping(), 'test', 'vuln') + f = result[0]['findings'][0] + assert 'pkg:unknown/' not in f['purl'] + + def test_dockerfile_findings_structure(self): + result = trivy_format(self._make_dockerfile_mapping(), 'test', 'dockerfile') + f = result[0]['findings'][0] + assert f['rule'] == 'DS001' + assert f['severity'] == 'high' + assert f['message'] == 'Use COPY instead of ADD' + assert f['resolution'] == 'Replace ADD with COPY' + assert f['scanner'] == 'trivy' + + def test_content_still_present(self): + result = trivy_format(self._make_vuln_mapping(), 'test', 'vuln') + assert 'content' in result[0] + assert 'title' in result[0] + + def test_empty_mapping_returns_empty_findings(self): + result = trivy_format({}, 'test', 'vuln') + assert result[0]['findings'] == [] + + +# --- Socket Tier1 --- + +class TestTier1WebhookFindings: + def _make_components(self): + return [{ + 'name': 'lodash', + 'type': 'npm', + 'version': '4.17.20', + 'alerts': [{ + 'severity': 'high', + 'props': { + 'ghsaId': 'GHSA-xxxx-yyyy', + 'cveId': 'CVE-2021-23337', + 'reachability': 'reachable', + 'purl': 'pkg:npm/lodash@4.17.20', + }, + }], + }] + + def test_findings_key_present(self): + result = tier1_format(self._make_components()) + assert 'findings' in result[0] + + def test_findings_structure(self): + result = tier1_format(self._make_components()) + f = result[0]['findings'][0] + assert f['package'] == 'lodash' + assert f['version'] == '4.17.20' + assert f['purl'] == 'pkg:npm/lodash@4.17.20' + assert f['severity'] == 'high' + assert f['reachability'] == 'reachable' + assert f['scanner'] == 'socket-tier1' + + def test_empty_returns_empty_findings(self): + result = tier1_format([]) + assert result[0]['findings'] == [] + + +# --- OpenGrep --- + +class TestOpenGrepWebhookFindings: + def _make_groups(self): + return { + 'sast-python': [{ + 'component': {'name': 'app.py'}, + 'alert': { + 'severity': 'medium', + 'title': 'python.flask.security.injection', + 'props': { + 'ruleId': 'python.flask.security.injection', + 'filePath': '/src/app.py', + 'startLine': '10', + 'endLine': '12', + }, + }, + }] + } + + def test_findings_key_present(self): + result = opengrep_format(self._make_groups()) + assert 'findings' in result[0] + + def test_findings_structure(self): + result = opengrep_format(self._make_groups()) + f = result[0]['findings'][0] + assert f['rule'] == 'python.flask.security.injection' + assert f['severity'] == 'medium' + assert f['file'] == 'app.py' + assert f['path'] == '/src/app.py' + assert f['lines'] == '10-12' + assert f['language'] == 'sast-python' + assert f['scanner'] == 'opengrep' + + def test_empty_groups_returns_empty(self): + result = opengrep_format({}) + assert result == [] + + def test_empty_items_skipped(self): + result = opengrep_format({'sast-python': []}) + assert result == [] + + +# --- TruffleHog --- + +class TestTruffleHogWebhookFindings: + def _make_mapping(self): + return { + 'comp1': { + 'alerts': [{ + 'severity': 'high', + 'title': 'AWS', + 'props': { + 'detectorName': 'AWS', + 'filePath': '.env', + 'lineNumber': '5', + 'redactedValue': 'AKIA****XXXX', + 'verified': True, + 'secretType': 'aws_access_key', + }, + }], + } + } + + def test_findings_key_present(self): + result = trufflehog_format(self._make_mapping()) + assert 'findings' in result[0] + + def test_findings_structure(self): + result = trufflehog_format(self._make_mapping()) + f = result[0]['findings'][0] + assert f['detector'] == 'AWS' + assert f['severity'] == 'high' + assert f['file'] == '.env' + assert f['line'] == '5' + assert f['verified'] is True + assert f['scanner'] == 'trufflehog' + + def test_findings_omit_redacted_value(self): + result = trufflehog_format(self._make_mapping()) + f = result[0]['findings'][0] + assert 'redacted_value' not in f + assert 'redactedValue' not in f + + def test_markdown_still_has_redacted(self): + """The markdown content (for human reading) should still include redacted values.""" + result = trufflehog_format(self._make_mapping()) + assert 'AKIA****XXXX' in result[0]['content'] + + def test_empty_mapping_returns_empty_findings(self): + result = trufflehog_format({}) + assert result[0]['findings'] == [] From f452c09a61ff5dcc40e4260ca6e34a874609744b Mon Sep 17 00:00:00 2001 From: lelia Date: Mon, 2 Mar 2026 19:25:40 -0500 Subject: [PATCH 2/4] Bump version to stage release Signed-off-by: lelia --- README.md | 8 ++-- docs/github-action.md | 40 ++++++++++---------- docs/local-install-docker.md | 72 ++++++++++++++++++------------------ docs/pre-commit-hook.md | 2 +- pyproject.toml | 2 +- socket_basics/__init__.py | 2 +- socket_basics/version.py | 2 +- uv.lock | 2 +- 8 files changed, 65 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index c1f0b0f..3ddd400 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Socket Basics - uses: SocketDev/socket-basics@1.1.2 + uses: SocketDev/socket-basics@1.1.3 env: GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} with: @@ -144,10 +144,10 @@ For GitHub Actions, see the [Quick Start](#-quick-start---github-actions) above ```bash # Build with version tag -docker build -t socketdev/socket-basics:1.1.2 . +docker build -t socketdev/socket-basics:1.1.3 . # Run scan -docker run --rm -v "$PWD:/workspace" socketdev/socket-basics:1.1.2 \ +docker run --rm -v "$PWD:/workspace" socketdev/socket-basics:1.1.3 \ --workspace /workspace \ --python-sast-enabled \ --secret-scanning-enabled \ @@ -161,7 +161,7 @@ docker build \ --build-arg TRIVY_VERSION=v0.69.2 \ --build-arg TRUFFLEHOG_VERSION=v3.93.6 \ --build-arg OPENGREP_VERSION=v1.16.2 \ - -t socketdev/socket-basics:1.1.2 . + -t socketdev/socket-basics:1.1.3 . ``` 📖 **[View Docker Installation Guide](docs/local-install-docker.md)** diff --git a/docs/github-action.md b/docs/github-action.md index b9f4136..c3da16f 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -42,7 +42,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Socket Basics - uses: SocketDev/socket-basics@1.1.2 + uses: SocketDev/socket-basics@1.1.3 env: GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} with: @@ -77,7 +77,7 @@ Include these in your workflow's `jobs..permissions` section. **SAST (Static Analysis):** ```yaml -- uses: SocketDev/socket-basics@1.1.2 +- uses: SocketDev/socket-basics@1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} # Enable SAST for specific languages @@ -91,7 +91,7 @@ Include these in your workflow's `jobs..permissions` section. **Secret Scanning:** ```yaml -- uses: SocketDev/socket-basics@1.1.2 +- uses: SocketDev/socket-basics@1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} secret_scanning_enabled: 'true' @@ -103,7 +103,7 @@ Include these in your workflow's `jobs..permissions` section. **Container Scanning:** ```yaml -- uses: SocketDev/socket-basics@1.1.2 +- uses: SocketDev/socket-basics@1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} # Scan Docker images (auto-enables container scanning) @@ -114,7 +114,7 @@ Include these in your workflow's `jobs..permissions` section. **Socket Tier 1 Reachability:** ```yaml -- uses: SocketDev/socket-basics@1.1.2 +- uses: SocketDev/socket-basics@1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} socket_tier_1_enabled: 'true' @@ -123,7 +123,7 @@ Include these in your workflow's `jobs..permissions` section. ### Output Configuration ```yaml -- uses: SocketDev/socket-basics@1.1.2 +- uses: SocketDev/socket-basics@1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} python_sast_enabled: 'true' @@ -159,7 +159,7 @@ Configure Socket Basics centrally from the [Socket Dashboard](https://socket.dev **Enable in workflow:** ```yaml -- uses: SocketDev/socket-basics@1.1.2 +- uses: SocketDev/socket-basics@1.1.3 env: GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} with: @@ -171,7 +171,7 @@ Configure Socket Basics centrally from the [Socket Dashboard](https://socket.dev > **Note:** You can also pass credentials using environment variables instead of the `with:` section: > ```yaml -> - uses: SocketDev/socket-basics@1.1.2 +> - uses: SocketDev/socket-basics@1.1.3 > env: > SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_SECURITY_API_KEY }} > with: @@ -189,7 +189,7 @@ All notification integrations require Socket Enterprise. **Slack Notifications:** ```yaml -- uses: SocketDev/socket-basics@1.1.2 +- uses: SocketDev/socket-basics@1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} socket_org: ${{ secrets.SOCKET_ORG }} @@ -201,7 +201,7 @@ All notification integrations require Socket Enterprise. **Jira Issue Creation:** ```yaml -- uses: SocketDev/socket-basics@1.1.2 +- uses: SocketDev/socket-basics@1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} socket_org: ${{ secrets.SOCKET_ORG }} @@ -216,7 +216,7 @@ All notification integrations require Socket Enterprise. **Microsoft Teams:** ```yaml -- uses: SocketDev/socket-basics@1.1.2 +- uses: SocketDev/socket-basics@1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} socket_org: ${{ secrets.SOCKET_ORG }} @@ -228,7 +228,7 @@ All notification integrations require Socket Enterprise. **Generic Webhook:** ```yaml -- uses: SocketDev/socket-basics@1.1.2 +- uses: SocketDev/socket-basics@1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} socket_org: ${{ secrets.SOCKET_ORG }} @@ -240,7 +240,7 @@ All notification integrations require Socket Enterprise. **SIEM Integration:** ```yaml -- uses: SocketDev/socket-basics@1.1.2 +- uses: SocketDev/socket-basics@1.1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} socket_org: ${{ secrets.SOCKET_ORG }} @@ -276,7 +276,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Socket Basics - uses: SocketDev/socket-basics@1.1.2 + uses: SocketDev/socket-basics@1.1.3 env: GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} with: @@ -322,7 +322,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Full Security Scan - uses: SocketDev/socket-basics@1.1.2 + uses: SocketDev/socket-basics@1.1.3 env: GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} with: @@ -373,10 +373,10 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Build Docker Image - run: docker build -t myapp:1.1.2:${{ github.sha }} . + run: docker build -t myapp:1.1.3:${{ github.sha }} . - name: Scan Container - uses: SocketDev/socket-basics@1.1.2 + uses: SocketDev/socket-basics@1.1.3 env: GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} with: @@ -439,7 +439,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Socket Basics - uses: SocketDev/socket-basics@1.1.2 + uses: SocketDev/socket-basics@1.1.3 env: GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} with: @@ -491,7 +491,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Socket Basics - uses: SocketDev/socket-basics@1.1.2 + uses: SocketDev/socket-basics@1.1.3 env: GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} with: @@ -584,7 +584,7 @@ env: ```yaml steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - Must be first - - uses: SocketDev/socket-basics@1.1.2 + - uses: SocketDev/socket-basics@1.1.3 ``` ### PR Comments Not Appearing diff --git a/docs/local-install-docker.md b/docs/local-install-docker.md index 8ec01a4..db57b84 100644 --- a/docs/local-install-docker.md +++ b/docs/local-install-docker.md @@ -17,7 +17,7 @@ Run Socket Basics locally using Docker without installing any security tools on # 1. Clone and build git clone https://github.com/SocketDev/socket-basics.git cd socket-basics -docker build -t socket-basics:1.1.2 . +docker build -t socket-basics:1.1.3 . # 2. Create .env file with your credentials cat > .env << 'EOF' @@ -29,7 +29,7 @@ EOF docker run --rm \ -v "$PWD:/workspace" \ --env-file .env \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --python \ --secrets \ @@ -46,10 +46,10 @@ git clone https://github.com/SocketDev/socket-basics.git cd socket-basics # Build with version tag -docker build -t socket-basics:1.1.2 . +docker build -t socket-basics:1.1.3 . # Or build with latest tag -docker build -t socket-basics:1.1.2:latest . +docker build -t socket-basics:1.1.3:latest . # Verify the build docker images | grep socket-basics @@ -59,10 +59,10 @@ docker images | grep socket-basics ```bash # Use your own image name -docker build -t myorg/security-scanner:1.1.2 . +docker build -t myorg/security-scanner:1.1.3 . # Build for specific platform (e.g., for M1/M2 Macs) -docker build --platform linux/amd64 -t socket-basics:1.1.2 . +docker build --platform linux/amd64 -t socket-basics:1.1.3 . ``` ### Build with Custom Tool Versions @@ -74,7 +74,7 @@ docker build \ --build-arg TRIVY_VERSION=v0.69.2 \ --build-arg TRUFFLEHOG_VERSION=v3.93.6 \ --build-arg OPENGREP_VERSION=v1.16.2 \ - -t socket-basics:1.1.2 . + -t socket-basics:1.1.3 . ``` Omit any `--build-arg` to use the default version for that tool. For the app tests image, build from the `app_tests` directory and use the same build args. @@ -83,11 +83,11 @@ Omit any `--build-arg` to use the default version for that tool. For the app tes ```bash # Check that all tools are available in the container -docker run --rm socket-basics:1.1.2 socket-basics --version -docker run --rm socket-basics:1.1.2 socket --version -docker run --rm socket-basics:1.1.2 trivy --version -docker run --rm socket-basics:1.1.2 opengrep --version -docker run --rm socket-basics:1.1.2 trufflehog --version +docker run --rm socket-basics:1.1.3 socket-basics --version +docker run --rm socket-basics:1.1.3 socket --version +docker run --rm socket-basics:1.1.3 trivy --version +docker run --rm socket-basics:1.1.3 opengrep --version +docker run --rm socket-basics:1.1.3 trufflehog --version ``` ### Smoke Test @@ -122,7 +122,7 @@ Mount your project directory into the container: # Scan current directory docker run --rm \ -v "$PWD:/workspace" \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --python \ --secrets \ @@ -139,7 +139,7 @@ docker run --rm \ # Scan a specific project directory docker run --rm \ -v "/path/to/your/project:/workspace" \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --javascript \ --secrets @@ -150,7 +150,7 @@ docker run --rm \ ```bash docker run --rm \ -v "$PWD:/workspace" \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --all-languages \ --secrets \ @@ -198,7 +198,7 @@ VERBOSE=false docker run --rm \ -v "$PWD:/workspace" \ --env-file .env \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --python \ --secrets @@ -213,7 +213,7 @@ docker run --rm \ -v "$PWD:/workspace" \ -e "SOCKET_SECURITY_API_KEY=scrt_your_api_key" \ -e "SOCKET_ORG=your-org-slug" \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --python \ --secrets \ @@ -235,7 +235,7 @@ docker run --rm \ --env-file .env.socket \ --env-file .env.notifiers \ --env-file .env.scanning \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --all-languages ``` @@ -254,7 +254,7 @@ docker run --rm \ -v "$PWD:/workspace" \ -e "SOCKET_SECURITY_API_KEY=$SOCKET_SECURITY_API_KEY" \ -e "SOCKET_ORG=$SOCKET_ORG" \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --python ``` @@ -270,7 +270,7 @@ docker run --rm \ -v "$PWD:/workspace" \ -v "/var/run/docker.sock:/var/run/docker.sock" \ --env-file .env \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --images "nginx:latest,redis:7" \ --console-tabular-enabled @@ -291,7 +291,7 @@ docker run --rm \ -v "$PWD:/workspace" \ -v "$PWD/scan-results:/results" \ --env-file .env \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --python \ --secrets \ @@ -308,7 +308,7 @@ docker run --rm -it \ -v "$PWD:/workspace" \ --env-file .env \ --entrypoint /bin/bash \ - socket-basics:1.1.2 + socket-basics:1.1.3 # Inside container, run commands manually: # cd /workspace @@ -337,7 +337,7 @@ docker run --rm \ -v "$PWD:/workspace" \ -v "$PWD/socket-config.json:/config.json" \ --env-file .env \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --config /config.json ``` @@ -361,7 +361,7 @@ for PROJECT in "${PROJECTS[@]}"; do docker run --rm \ -v "$PROJECT:/workspace" \ --env-file .env \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --all-languages \ --secrets \ @@ -385,7 +385,7 @@ pipeline { stage('Security Scan') { steps { script { - docker.image('socket-basics:1.1.2').inside( + docker.image('socket-basics:1.1.3').inside( "-v ${WORKSPACE}:/workspace --env-file .env" ) { sh ''' @@ -407,7 +407,7 @@ pipeline { ```yaml security-scan: - image: socket-basics:1.1.2 + image: socket-basics:1.1.3 stage: test script: - socket-basics @@ -433,7 +433,7 @@ security-scan: docker run --rm \ -v "$PWD:/workspace" \ --user "$(id -u):$(id -g)" \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace ``` @@ -452,14 +452,14 @@ security-scan: ```bash docker run --rm \ -v "$(pwd):/workspace" \ # Use $(pwd) instead of $PWD - socket-basics:1.1.2 + socket-basics:1.1.3 ``` 2. Verify mount: ```bash docker run --rm \ -v "$PWD:/workspace" \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ ls -la /workspace ``` @@ -489,7 +489,7 @@ security-scan: docker run --rm \ -v "$PWD:/workspace" \ --env-file "$(pwd)/.env" \ - socket-basics:1.1.2 + socket-basics:1.1.3 ``` ### Docker Socket Permission Denied @@ -537,7 +537,7 @@ security-scan: ```bash docker run --rm \ -v "$PWD:/workspace" \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --python \ --secrets \ @@ -558,7 +558,7 @@ security-scan: ```bash docker run --rm \ -v "$PWD:/workspace" \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --output /workspace/results.json # Save to mounted directory ``` @@ -569,7 +569,7 @@ security-scan: docker run --rm \ -v "$PWD:/workspace" \ -v "$PWD/results:/results" \ - socket-basics:1.1.2 \ + socket-basics:1.1.3 \ --workspace /workspace \ --output /results/scan.json ``` @@ -580,14 +580,14 @@ Add these to your `~/.bashrc` or `~/.zshrc` for quick access: ```bash # Socket Basics Docker aliases -alias sb-docker='docker run --rm -v "$PWD:/workspace" --env-file .env socket-basics:1.1.2 --workspace /workspace' +alias sb-docker='docker run --rm -v "$PWD:/workspace" --env-file .env socket-basics:1.1.3 --workspace /workspace' alias sb-quick='sb-docker --secrets --console-tabular-enabled' alias sb-python='sb-docker --python --secrets --console-tabular-enabled' alias sb-js='sb-docker --javascript --secrets --console-tabular-enabled' alias sb-all='sb-docker --all-languages --secrets --socket-tier1 --console-tabular-enabled' # Rebuild image -alias sb-build='docker build -t socket-basics:1.1.2 .' +alias sb-build='docker build -t socket-basics:1.1.3 .' ``` Usage: @@ -623,7 +623,7 @@ set -e # Configuration PROJECT_DIR="$(pwd)" RESULTS_DIR="./scan-results" -IMAGE_NAME="socket-basics:1.1.2" +IMAGE_NAME="socket-basics:1.1.3" ENV_FILE=".env" # Create results directory diff --git a/docs/pre-commit-hook.md b/docs/pre-commit-hook.md index 932ade7..a56cf69 100644 --- a/docs/pre-commit-hook.md +++ b/docs/pre-commit-hook.md @@ -39,7 +39,7 @@ git clone https://github.com/SocketDev/socket-basics.git cd socket-basics # Build the Docker image with version tag -docker build -t socket-basics:1.1.2 . +docker build -t socket-basics:1.1.3 . ``` **2. Create pre-commit hook:** diff --git a/pyproject.toml b/pyproject.toml index cc0cd96..f424918 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "socket_basics" -version = "1.1.2" +version = "1.1.3" description = "Socket Basics with integrated SAST, secret scanning, and container analysis" readme = "README.md" requires-python = ">=3.10" diff --git a/socket_basics/__init__.py b/socket_basics/__init__.py index 0dc2d9e..92098b9 100644 --- a/socket_basics/__init__.py +++ b/socket_basics/__init__.py @@ -12,7 +12,7 @@ from .socket_basics import SecurityScanner, main from .core.config import load_config_from_env, Config -__version__ = "1.1.2" +__version__ = "1.1.3" __author__ = "Socket.dev" __email__ = "support@socket.dev" diff --git a/socket_basics/version.py b/socket_basics/version.py index 72f26f5..0b2f79d 100644 --- a/socket_basics/version.py +++ b/socket_basics/version.py @@ -1 +1 @@ -__version__ = "1.1.2" +__version__ = "1.1.3" diff --git a/uv.lock b/uv.lock index 9048948..0f9933a 100644 --- a/uv.lock +++ b/uv.lock @@ -623,7 +623,7 @@ wheels = [ [[package]] name = "socket-basics" -version = "1.1.2" +version = "1.1.3" source = { editable = "." } dependencies = [ { name = "jsonschema" }, From 207d69d482e2cb6db99c833eb1c908297cbf7a25 Mon Sep 17 00:00:00 2001 From: lelia Date: Mon, 2 Mar 2026 19:26:09 -0500 Subject: [PATCH 3/4] Use plural (cves) for uniform behavior Signed-off-by: lelia --- socket_basics/core/connector/socket_tier1/webhook.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/socket_basics/core/connector/socket_tier1/webhook.py b/socket_basics/core/connector/socket_tier1/webhook.py index 92c5b85..dd4eb8c 100644 --- a/socket_basics/core/connector/socket_tier1/webhook.py +++ b/socket_basics/core/connector/socket_tier1/webhook.py @@ -52,7 +52,7 @@ def format_notifications(components_list: List[Dict[str, Any]]) -> List[Dict[str 'package': comp_name, 'version': str(comp.get('version', '')), 'purl': purl, - 'cve': cve_id, + 'cves': [cve_id] if cve_id else [], 'severity': severity, 'reachability': reachability, 'scanner': 'socket-tier1', @@ -75,4 +75,4 @@ def format_notifications(components_list: List[Dict[str, Any]]) -> List[Dict[str 'title': 'Socket Tier1 Reachability Analysis', 'content': content, 'findings': findings, - }] \ No newline at end of file + }] From 31c5304814e519d8147a86c5b14567866dda5084 Mon Sep 17 00:00:00 2001 From: lelia Date: Mon, 2 Mar 2026 19:26:21 -0500 Subject: [PATCH 4/4] Update tests to account for new cves syntax Signed-off-by: lelia --- tests/test_webhook_payload.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_webhook_payload.py b/tests/test_webhook_payload.py index 9282dba..3aba4f5 100644 --- a/tests/test_webhook_payload.py +++ b/tests/test_webhook_payload.py @@ -102,6 +102,7 @@ def test_findings_structure(self): assert f['package'] == 'lodash' assert f['version'] == '4.17.20' assert f['purl'] == 'pkg:npm/lodash@4.17.20' + assert f['cves'] == ['GHSA-xxxx-yyyy'] assert f['severity'] == 'high' assert f['reachability'] == 'reachable' assert f['scanner'] == 'socket-tier1'