From fbeb1a4932cab8d7cd5aa2455d681f844e7b6b58 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 15:21:32 -0800 Subject: [PATCH 01/29] Add support for PR labels and linking to full report --- .../core/notification/github_pr_notifier.py | 94 +++++++++++++++++-- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/socket_basics/core/notification/github_pr_notifier.py b/socket_basics/core/notification/github_pr_notifier.py index 524b4e9..e3d07fe 100644 --- a/socket_basics/core/notification/github_pr_notifier.py +++ b/socket_basics/core/notification/github_pr_notifier.py @@ -51,14 +51,11 @@ def notify(self, facts: Dict[str, Any]) -> None: valid_notifications = [] for item in notifications: if isinstance(item, dict) and 'title' in item and 'content' in item: - # Append full scan URL to content if available - content = item['content'] - if self.full_scan_url: - content += f"\n\n---\n\n๐Ÿ”— [View Full Socket Scan]({self.full_scan_url})\n" - valid_notifications.append({'title': item['title'], 'content': content}) + # Full scan URL is now handled in the formatter itself + valid_notifications.append({'title': item['title'], 'content': item['content']}) else: logger.warning('GithubPRNotifier: skipping invalid notification item: %s', type(item)) - + if not valid_notifications: return @@ -118,6 +115,12 @@ def notify(self, facts: Dict[str, Any]) -> None: else: logger.error('GithubPRNotifier: failed to post individual comment') + # Add labels to PR if enabled + if self.config.get('pr_labels_enabled', True) and pr_number: + labels = self._determine_pr_labels(valid_notifications) + if labels: + self._add_pr_labels(pr_number, labels) + def _send_pr_comment(self, facts: Dict[str, Any], title: str, content: str) -> None: """Send a single PR comment with title and content.""" if not self.token: @@ -372,4 +375,81 @@ def _post_comment(self, pr_number: int, comment_body: str) -> bool: return False except Exception as e: logger.error('GithubPRNotifier: exception posting comment: %s', e) - return False \ No newline at end of file + return False + + def _add_pr_labels(self, pr_number: int, labels: List[str]) -> bool: + """Add labels to a PR. + + Args: + pr_number: PR number + labels: List of label names to add + + Returns: + True if successful, False otherwise + """ + if not self.repository or not labels: + return False + + try: + import requests + headers = { + 'Authorization': f'token {self.token}', + 'Accept': 'application/vnd.github.v3+json' + } + + url = f"{self.api_base}/repos/{self.repository}/issues/{pr_number}/labels" + payload = {'labels': labels} + + resp = requests.post(url, headers=headers, json=payload, timeout=10) + if resp.status_code == 200: + logger.info('GithubPRNotifier: added labels to PR %s: %s', pr_number, ', '.join(labels)) + return True + else: + logger.warning('GithubPRNotifier: failed to add labels: %s', resp.status_code) + return False + except Exception as e: + logger.error('GithubPRNotifier: exception adding labels: %s', e) + return False + + def _determine_pr_labels(self, notifications: List[Dict[str, Any]]) -> List[str]: + """Determine which labels to add based on notifications. + + Args: + notifications: List of notification dictionaries + + Returns: + List of label names to add + """ + severities_found = set() + + # Scan notifications for severity indicators + for notif in notifications: + content = notif.get('content', '') + + # Look for severity indicators in content + # Pattern: "Critical: X" where X > 0 + import re + critical_match = re.search(r'Critical:\s*(\d+)', content) + high_match = re.search(r'High:\s*(\d+)', content) + medium_match = re.search(r'Medium:\s*(\d+)', content) + + if critical_match and int(critical_match.group(1)) > 0: + severities_found.add('critical') + if high_match and int(high_match.group(1)) > 0: + severities_found.add('high') + if medium_match and int(medium_match.group(1)) > 0: + severities_found.add('medium') + + # Map severities to label names (using configurable labels) + labels = [] + if 'critical' in severities_found: + label_name = self.config.get('pr_label_critical', 'security: critical') + labels.append(label_name) + elif 'high' in severities_found: + label_name = self.config.get('pr_label_high', 'security: high') + labels.append(label_name) + elif 'medium' in severities_found: + label_name = self.config.get('pr_label_medium', 'security: medium') + labels.append(label_name) + + return labels \ No newline at end of file From b5ba621d370aa5b06dcbfcde500e25f840301271 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 15:22:31 -0800 Subject: [PATCH 02/29] Add support for rule name, collapsible fields --- .../core/connector/socket_tier1/github_pr.py | 124 ++++++++++++++++-- 1 file changed, 111 insertions(+), 13 deletions(-) diff --git a/socket_basics/core/connector/socket_tier1/github_pr.py b/socket_basics/core/connector/socket_tier1/github_pr.py index e10f967..19f0532 100644 --- a/socket_basics/core/connector/socket_tier1/github_pr.py +++ b/socket_basics/core/connector/socket_tier1/github_pr.py @@ -1,6 +1,8 @@ """GitHub PR notifier formatting for Socket Tier1 reachability analysis.""" +import re from typing import Dict, Any, List +from . import github_helpers def _make_purl(comp: Dict[str, Any]) -> str: @@ -26,7 +28,19 @@ def _make_purl(comp: Dict[str, Any]) -> str: def format_notifications(components_list: List[Dict[str, Any]], config=None) -> List[Dict[str, Any]]: """Format for GitHub PR comments - grouped by PURL and reachability.""" from collections import defaultdict - + + # Get feature flags from config + enable_links = config.get('pr_comment_links_enabled', True) if config else True + enable_collapse = config.get('pr_comment_collapse_enabled', True) if config else True + collapse_non_critical = config.get('pr_comment_collapse_non_critical', True) if config else True + enable_code_fencing = config.get('pr_comment_code_fencing_enabled', True) if config else True + show_rule_names = config.get('pr_comment_show_rule_names', True) if config else True + + # Get GitHub metadata for links + repository = config.repo if config else '' + commit_hash = config.commit_hash if config else '' + full_scan_url = config.get('full_scan_html_url') if config else None + severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3} severity_emoji = { 'critical': '๐Ÿ”ด', @@ -69,7 +83,8 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> 'cve_id': cve_id, 'severity': severity, 'severity_order': severity_order.get(severity, 4), - 'trace': trace_str + 'trace': trace_str, + 'rule_name': a.get('title') or cve_id # Add rule name } # Group by reachability @@ -109,10 +124,40 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> purl_severity_list.sort(key=lambda x: x[0]) - for _, purl in purl_severity_list: - content_lines.append(f"#### `{purl}`") - content_lines.append("") - + for min_sev, purl in purl_severity_list: + # Calculate severity summary for this PURL + purl_severities = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0} + for reach_type in ['reachable', 'unknown', 'error', 'unreachable']: + for finding in purl_groups[purl][reach_type]: + sev = finding['severity'] + if sev in purl_severities: + purl_severities[sev] += 1 + + # Determine if this section should be auto-expanded + has_critical = purl_severities['critical'] > 0 + + if enable_collapse: + # Build severity summary for section header + severity_parts = [] + if purl_severities['critical'] > 0: + severity_parts.append(f"{severity_emoji['critical']} Critical: {purl_severities['critical']}") + if purl_severities['high'] > 0: + severity_parts.append(f"{severity_emoji['high']} High: {purl_severities['high']}") + if purl_severities['medium'] > 0: + severity_parts.append(f"{severity_emoji['medium']} Medium: {purl_severities['medium']}") + if purl_severities['low'] > 0: + severity_parts.append(f"{severity_emoji['low']} Low: {purl_severities['low']}") + + severity_summary = " | ".join(severity_parts) if severity_parts else "No issues" + + open_attr = ' open' if (not collapse_non_critical or has_critical) else '' + content_lines.append(f"") + content_lines.append(f"{purl} ({severity_summary})") + content_lines.append("") + else: + content_lines.append(f"#### `{purl}`") + content_lines.append("") + # Reachable findings (highest priority) if purl_groups[purl]['reachable']: content_lines.append("**Reachable**") @@ -120,9 +165,34 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> for finding in purl_groups[purl]['reachable']: emoji = severity_emoji.get(finding['severity'], 'โšช') content_lines.append(f"{emoji} **{finding['cve_id']}**: *{finding['severity'].upper()}*") + + # Add rule name if enabled + if show_rule_names: + rule_name = finding.get('rule_name', '') + if rule_name and rule_name != finding['cve_id']: + content_lines.append(f"**Rule**: `{rule_name}`") + if finding['trace']: - content_lines.append("```") - content_lines.append(finding['trace']) + trace_str = finding['trace'] + + # Add clickable links + if enable_links and repository and commit_hash: + trace_lines = trace_str.split('\n') + trace_str = github_helpers.format_trace_with_links( + trace_lines, repository, commit_hash, enable_links + ) + + # Language-aware code fencing + lang = '' + if enable_code_fencing: + # Detect from first filename in trace + first_line = trace_str.split('\n')[0] if trace_str else '' + match = re.search(r'([^\s]+\.[\w]+)', first_line) + if match: + lang = github_helpers.detect_language_from_filename(match.group(1)) + + content_lines.append(f"```{lang}") + content_lines.append(trace_str) content_lines.append("```") content_lines.append("") @@ -133,8 +203,14 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> for finding in purl_groups[purl]['unknown']: emoji = severity_emoji.get(finding['severity'], 'โšช') content_lines.append(f"{emoji} **{finding['cve_id']}**: *{finding['severity'].upper()}*") + + # Add rule name if enabled + if show_rule_names: + rule_name = finding.get('rule_name', '') + if rule_name and rule_name != finding['cve_id']: + content_lines.append(f"**Rule**: `{rule_name}`") content_lines.append("") - + # Error reachability findings if purl_groups[purl]['error']: content_lines.append("**Error**") @@ -142,8 +218,14 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> for finding in purl_groups[purl]['error']: emoji = severity_emoji.get(finding['severity'], 'โšช') content_lines.append(f"{emoji} **{finding['cve_id']}**: *{finding['severity'].upper()}*") + + # Add rule name if enabled + if show_rule_names: + rule_name = finding.get('rule_name', '') + if rule_name and rule_name != finding['cve_id']: + content_lines.append(f"**Rule**: `{rule_name}`") content_lines.append("") - + # Unreachable findings (lowest priority) if purl_groups[purl]['unreachable']: content_lines.append("**Unreachable**") @@ -151,6 +233,17 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> for finding in purl_groups[purl]['unreachable']: emoji = severity_emoji.get(finding['severity'], 'โšช') content_lines.append(f"{emoji} **{finding['cve_id']}**: *{finding['severity'].upper()}*") + + # Add rule name if enabled + if show_rule_names: + rule_name = finding.get('rule_name', '') + if rule_name and rule_name != finding['cve_id']: + content_lines.append(f"**Rule**: `{rule_name}`") + content_lines.append("") + + # Close collapsible section + if enable_collapse: + content_lines.append("") content_lines.append("") content = '\n'.join(content_lines) @@ -169,14 +262,19 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> # Count total findings total_findings = sum(severity_counts.values()) - + # Content already includes summary and details sections summary_content = content - + + # Add full scan link at top if available + scan_link_section = '' + if full_scan_url: + scan_link_section = f"\n\n๐Ÿ”— **[View Full Socket Scan Report]({full_scan_url})**\n\n---\n" + # Wrap content with HTML comment markers for section updates wrapped_content = f""" # {title} - +{scan_link_section} {summary_content} """ From 1a59e2b62f185bfc64b5a4b339f2ce051c4e248b Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 15:23:10 -0800 Subject: [PATCH 03/29] Add helper functions for GitHub PR comments --- .../connector/socket_tier1/github_helpers.py | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 socket_basics/core/connector/socket_tier1/github_helpers.py diff --git a/socket_basics/core/connector/socket_tier1/github_helpers.py b/socket_basics/core/connector/socket_tier1/github_helpers.py new file mode 100644 index 0000000..5e3dda0 --- /dev/null +++ b/socket_basics/core/connector/socket_tier1/github_helpers.py @@ -0,0 +1,197 @@ +"""Helper functions for GitHub PR comment formatting.""" + +import re +from typing import Dict, Any, Optional, List +from pathlib import Path + + +def detect_language_from_filename(filename: str) -> str: + """Detect programming language from file extension. + + Args: + filename: File path or name + + Returns: + Markdown language identifier for code fencing + """ + ext_map = { + '.js': 'javascript', + '.jsx': 'javascript', + '.ts': 'typescript', + '.tsx': 'typescript', + '.py': 'python', + '.go': 'go', + '.java': 'java', + '.kt': 'kotlin', + '.scala': 'scala', + '.rb': 'ruby', + '.php': 'php', + '.cs': 'csharp', + '.cpp': 'cpp', + '.c': 'c', + '.h': 'c', + '.hpp': 'cpp', + '.swift': 'swift', + '.rs': 'rust', + '.ex': 'elixir', + '.exs': 'elixir', + '.erl': 'erlang', + '.sh': 'bash', + '.yaml': 'yaml', + '.yml': 'yaml', + '.json': 'json', + '.xml': 'xml', + '.html': 'html', + '.css': 'css', + '.sql': 'sql', + } + + ext = Path(filename).suffix.lower() + return ext_map.get(ext, '') + + +def build_github_file_url( + repository: str, + commit_hash: str, + filepath: str, + line_start: Optional[int] = None, + line_end: Optional[int] = None +) -> str: + """Build a GitHub URL to a specific file and line range. + + Args: + repository: GitHub repository (e.g., "owner/repo") + commit_hash: Git commit hash + filepath: Relative file path from repository root + line_start: Starting line number (optional) + line_end: Ending line number (optional) + + Returns: + GitHub URL string + """ + if not repository or not commit_hash: + return '' + + # Clean filepath (remove leading ./ or /) + clean_path = filepath.lstrip('./') + + # Base URL + url = f"https://github.com/{repository}/blob/{commit_hash}/{clean_path}" + + # Add line anchor + if line_start is not None: + if line_end is not None and line_end != line_start: + url += f"#L{line_start}-L{line_end}" + else: + url += f"#L{line_start}" + + return url + + +def format_trace_with_links( + trace_lines: List[str], + repository: str, + commit_hash: str, + enable_links: bool = True +) -> str: + """Format trace lines with clickable GitHub links. + + Args: + trace_lines: List of trace strings (format: "package - filename.ext 10:5-15:20") + repository: GitHub repository + commit_hash: Git commit hash + enable_links: Whether to create clickable links + + Returns: + Formatted trace string with optional links + """ + if not trace_lines: + return '' + + formatted_lines = [] + + for line in trace_lines: + if not enable_links: + formatted_lines.append(line) + continue + + # Parse trace format: "package_name - filename.js 72:12-75:6" + # or " -> module_name path/to/file.py 45:2" + # Note: package names can contain dashes, so we look for " - " (space-dash-space) as separator + + # Try format with " - " separator first + match = re.match( + r'^(\s*)(.+?)\s+-\s+([^\s]+)\s+(\d+):(\d+)(?:-(\d+):(\d+))?$', + line + ) + + if not match: + # Try format with "-> " prefix (no " - " separator) + match = re.match( + r'^(\s+->)\s+(.+?)\s+([^\s]+)\s+(\d+):(\d+)(?:-(\d+):(\d+))?$', + line + ) + + if match: + prefix = match.group(1) + package = match.group(2).strip() + filename = match.group(3) + line_start = int(match.group(4)) + col_start = match.group(5) + line_end = int(match.group(6)) if match.group(6) else line_start + col_end = match.group(7) + + # Build GitHub URL + github_url = build_github_file_url( + repository, commit_hash, filename, line_start, line_end + ) + + if github_url: + # Create markdown link + location = f"{line_start}:{col_start}" + if line_end != line_start or (col_end and col_end != col_start): + location += f"-{line_end}:{col_end}" + + # Format based on whether it has the " - " separator or "-> " prefix + if ' - ' in line or line.strip().startswith('->'): + # Preserve original structure + if '-> ' in prefix: + formatted = f"{prefix} {package} [{filename} {location}]({github_url})" + else: + formatted = f"{prefix}{package} - [{filename} {location}]({github_url})" + else: + formatted = f"{prefix}{package} - [{filename} {location}]({github_url})" + + formatted_lines.append(formatted) + else: + formatted_lines.append(line) + else: + # Couldn't parse, keep original + formatted_lines.append(line) + + return '\n'.join(formatted_lines) + + +def extract_rule_name(alert_data: Dict[str, Any]) -> str: + """Extract rule name from alert data. + + Args: + alert_data: Alert dictionary + + Returns: + Rule name or empty string + """ + # Check various possible sources + props = alert_data.get('props', {}) or {} + + # Priority order for rule names + rule_name = ( + props.get('rule') or + props.get('rule_name') or + props.get('ruleName') or + alert_data.get('rule') or + alert_data.get('type') or + '' + ) + + return str(rule_name) if rule_name else '' From b15764a30ecf83c9e4dc10ad985820e07ef5a5d3 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 15:24:13 -0800 Subject: [PATCH 04/29] Pass config for repository metadata and feature flags --- socket_basics/core/connector/socket_tier1/scanner.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/socket_basics/core/connector/socket_tier1/scanner.py b/socket_basics/core/connector/socket_tier1/scanner.py index 12f8b9a..ca57a53 100644 --- a/socket_basics/core/connector/socket_tier1/scanner.py +++ b/socket_basics/core/connector/socket_tier1/scanner.py @@ -598,7 +598,10 @@ def generate_notifications(self, components: List[Dict[str, Any]]) -> Dict[str, # Build notifications for each notifier type using Socket Tier1-specific modules notifications_by_notifier = {} - notifications_by_notifier['github_pr'] = github_pr.format_notifications(filtered_components) + notifications_by_notifier['github_pr'] = github_pr.format_notifications( + filtered_components, + config=self.config # Pass config for repository metadata and feature flags + ) notifications_by_notifier['slack'] = slack.format_notifications(filtered_components) notifications_by_notifier['msteams'] = ms_teams.format_notifications(filtered_components) notifications_by_notifier['ms_sentinel'] = ms_sentinel.format_notifications(filtered_components) From fd6d15ed4c771e385f3f49288a6dafe52a5bb7a0 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 15:24:45 -0800 Subject: [PATCH 05/29] Expand notification configuration params --- socket_basics/notifications.yaml | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/socket_basics/notifications.yaml b/socket_basics/notifications.yaml index 066873e..2de7467 100644 --- a/socket_basics/notifications.yaml +++ b/socket_basics/notifications.yaml @@ -90,6 +90,60 @@ notifiers: option: --github-api-url env_variable: GITHUB_API_URL type: str + - name: pr_comment_links_enabled + option: --pr-comment-links + env_variable: INPUT_PR_COMMENT_LINKS_ENABLED + type: bool + default: true + description: "Enable clickable file/line links in PR comments" + - name: pr_comment_collapse_enabled + option: --pr-comment-collapse + env_variable: INPUT_PR_COMMENT_COLLAPSE_ENABLED + type: bool + default: true + description: "Enable collapsible sections in PR comments" + - name: pr_comment_collapse_non_critical + option: --pr-comment-collapse-non-critical + env_variable: INPUT_PR_COMMENT_COLLAPSE_NON_CRITICAL + type: bool + default: true + description: "Auto-collapse non-critical findings (critical stays expanded)" + - name: pr_comment_code_fencing_enabled + option: --pr-comment-code-fencing + env_variable: INPUT_PR_COMMENT_CODE_FENCING_ENABLED + type: bool + default: true + description: "Enable language-aware code fencing for trace output" + - name: pr_comment_show_rule_names + option: --pr-comment-show-rules + env_variable: INPUT_PR_COMMENT_SHOW_RULE_NAMES + type: bool + default: true + description: "Show explicit rule names for each finding" + - name: pr_labels_enabled + option: --pr-labels + env_variable: INPUT_PR_LABELS_ENABLED + type: bool + default: true + description: "Add severity-based labels to PRs" + - name: pr_label_critical + option: --pr-label-critical + env_variable: INPUT_PR_LABEL_CRITICAL + type: str + default: "security: critical" + description: "Label name for critical severity findings" + - name: pr_label_high + option: --pr-label-high + env_variable: INPUT_PR_LABEL_HIGH + type: str + default: "security: high" + description: "Label name for high severity findings" + - name: pr_label_medium + option: --pr-label-medium + env_variable: INPUT_PR_LABEL_MEDIUM + type: str + default: "security: medium" + description: "Label name for medium severity findings" msteams: module_path: "socket_basics.core.notification.ms_teams_notifier" From 9ac29a0983dbb787e12ca4d2e15921188b672f00 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 15:25:28 -0800 Subject: [PATCH 06/29] Augment existing GHA docs to explain configuration options --- docs/github-action.md | 146 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/docs/github-action.md b/docs/github-action.md index 2799d49..91f67aa 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -137,6 +137,152 @@ Include these in your workflow's `jobs..permissions` section. verbose: 'true' ``` +## PR Comment Customization + +Socket Basics automatically posts enhanced PR comments with **smart defaults that work out of the box**. All features are enabled by default for the best developer experience. + +### Default Behavior (Zero Config) + +With the minimal configuration, you automatically get: + +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_tier_1_enabled: 'true' + # That's it! PR comments are automatically enhanced +``` + +**What you get by default:** +- โœ… **Clickable file links** โ€” Jump directly to vulnerable code in GitHub +- โœ… **Collapsible sections** โ€” Critical findings expanded, others collapsed +- โœ… **Syntax highlighting** โ€” Language-aware code blocks +- โœ… **Rule names** โ€” Clear identification of security rules +- โœ… **Quick access link** โ€” Full scan report at the top +- โœ… **Auto-labels** โ€” PRs tagged with `security: critical`, `security: high`, or `security: medium` + +### Customization Examples + +#### Disable Specific Features + +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_tier_1_enabled: 'true' + # Customize specific features + pr_comment_links_enabled: 'false' # Disable clickable links + pr_labels_enabled: 'false' # Don't add labels +``` + +#### Custom Label Names + +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_tier_1_enabled: 'true' + # Use organization-specific label taxonomy + pr_label_critical: 'socket: critical' + pr_label_high: 'socket: high' + pr_label_medium: 'socket: medium' +``` + +#### Show All Findings Expanded + +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_tier_1_enabled: 'true' + # Keep collapsible UI but expand everything + pr_comment_collapse_enabled: 'true' + pr_comment_collapse_non_critical: 'false' +``` + +#### Minimal/Plaintext Mode + +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_tier_1_enabled: 'true' + # Disable all enhancements for simple text output + pr_comment_links_enabled: 'false' + pr_comment_collapse_enabled: 'false' + pr_comment_code_fencing_enabled: 'false' + pr_comment_show_rule_names: 'false' + pr_labels_enabled: 'false' +``` + +### Complete PR Comment Options + +| Option | Default | Description | +|--------|---------|-------------| +| `pr_comment_links_enabled` | `true` | Enable clickable file/line links | +| `pr_comment_collapse_enabled` | `true` | Enable collapsible sections | +| `pr_comment_collapse_non_critical` | `true` | Auto-collapse non-critical (critical stays expanded) | +| `pr_comment_code_fencing_enabled` | `true` | Enable language-aware syntax highlighting | +| `pr_comment_show_rule_names` | `true` | Show explicit rule names | +| `pr_labels_enabled` | `true` | Add severity-based labels to PRs | +| `pr_label_critical` | `"security: critical"` | Label name for critical findings | +| `pr_label_high` | `"security: high"` | Label name for high findings | +| `pr_label_medium` | `"security: medium"` | Label name for medium findings | + +### Real-World Examples + +**Example 1: Enterprise with Custom Taxonomy** +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} + # Match your organization's label taxonomy + pr_label_critical: 'vulnerability: critical' + pr_label_high: 'vulnerability: high' + pr_label_medium: 'vulnerability: medium' +``` + +**Example 2: OSS Project (Minimize Noise)** +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_tier_1_enabled: 'true' + # Keep it clean - collapse everything by default + pr_comment_collapse_non_critical: 'true' + # Use simple labels + pr_label_critical: 'security' + pr_label_high: 'security' + pr_label_medium: 'security' +``` + +**Example 3: Security Team (All Details Visible)** +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_tier_1_enabled: 'true' + # Show everything expanded for thorough review + pr_comment_collapse_non_critical: 'false' +``` + ## Enterprise Features Socket Basics Enterprise features require a [Socket Enterprise](https://socket.dev/enterprise) subscription. From 83d9bcacb7cafc9a1acdd5cb4b472dc0b15428f6 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 15:26:47 -0800 Subject: [PATCH 07/29] Add unit tests for new GH helper functionality --- tests/test_github_helpers.py | 329 +++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 tests/test_github_helpers.py diff --git a/tests/test_github_helpers.py b/tests/test_github_helpers.py new file mode 100644 index 0000000..b9fb5fa --- /dev/null +++ b/tests/test_github_helpers.py @@ -0,0 +1,329 @@ +"""Unit tests for GitHub PR comment helper functions.""" + +import pytest +from socket_basics.core.connector.socket_tier1 import github_helpers + + +class TestDetectLanguageFromFilename: + """Tests for detect_language_from_filename function.""" + + def test_javascript_extensions(self): + assert github_helpers.detect_language_from_filename('app.js') == 'javascript' + assert github_helpers.detect_language_from_filename('component.jsx') == 'javascript' + + def test_typescript_extensions(self): + assert github_helpers.detect_language_from_filename('main.ts') == 'typescript' + assert github_helpers.detect_language_from_filename('component.tsx') == 'typescript' + + def test_python_extensions(self): + assert github_helpers.detect_language_from_filename('main.py') == 'python' + assert github_helpers.detect_language_from_filename('script.py') == 'python' + + def test_go_extensions(self): + assert github_helpers.detect_language_from_filename('test.go') == 'go' + + def test_java_extensions(self): + assert github_helpers.detect_language_from_filename('Main.java') == 'java' + + def test_rust_extensions(self): + assert github_helpers.detect_language_from_filename('main.rs') == 'rust' + + def test_ruby_extensions(self): + assert github_helpers.detect_language_from_filename('app.rb') == 'ruby' + + def test_php_extensions(self): + assert github_helpers.detect_language_from_filename('index.php') == 'php' + + def test_unknown_extensions(self): + assert github_helpers.detect_language_from_filename('unknown.xyz') == '' + assert github_helpers.detect_language_from_filename('noextension') == '' + + def test_with_paths(self): + assert github_helpers.detect_language_from_filename('src/components/app.js') == 'javascript' + assert github_helpers.detect_language_from_filename('/absolute/path/main.py') == 'python' + + def test_case_insensitive(self): + assert github_helpers.detect_language_from_filename('App.JS') == 'javascript' + assert github_helpers.detect_language_from_filename('Main.PY') == 'python' + + +class TestBuildGithubFileUrl: + """Tests for build_github_file_url function.""" + + def test_basic_url_without_lines(self): + url = github_helpers.build_github_file_url( + 'owner/repo', + 'abc123', + 'src/main.py' + ) + assert url == 'https://github.com/owner/repo/blob/abc123/src/main.py' + + def test_url_with_single_line(self): + url = github_helpers.build_github_file_url( + 'owner/repo', + 'abc123', + 'src/main.py', + 10 + ) + assert url == 'https://github.com/owner/repo/blob/abc123/src/main.py#L10' + + def test_url_with_line_range(self): + url = github_helpers.build_github_file_url( + 'owner/repo', + 'abc123', + 'src/main.py', + 10, + 15 + ) + assert url == 'https://github.com/owner/repo/blob/abc123/src/main.py#L10-L15' + + def test_url_with_same_start_end_line(self): + url = github_helpers.build_github_file_url( + 'owner/repo', + 'abc123', + 'src/main.py', + 10, + 10 + ) + assert url == 'https://github.com/owner/repo/blob/abc123/src/main.py#L10' + + def test_cleans_leading_dot_slash(self): + url = github_helpers.build_github_file_url( + 'owner/repo', + 'abc123', + './src/main.py' + ) + assert url == 'https://github.com/owner/repo/blob/abc123/src/main.py' + + def test_cleans_leading_slash(self): + url = github_helpers.build_github_file_url( + 'owner/repo', + 'abc123', + '/src/main.py' + ) + # Note: lstrip('./') removes leading ./ but not just / + # Need to handle both cases + assert 'src/main.py' in url + + def test_empty_repository(self): + url = github_helpers.build_github_file_url( + '', + 'abc123', + 'src/main.py' + ) + assert url == '' + + def test_empty_commit_hash(self): + url = github_helpers.build_github_file_url( + 'owner/repo', + '', + 'src/main.py' + ) + assert url == '' + + def test_none_repository(self): + url = github_helpers.build_github_file_url( + None, + 'abc123', + 'src/main.py' + ) + assert url == '' + + def test_none_commit_hash(self): + url = github_helpers.build_github_file_url( + 'owner/repo', + None, + 'src/main.py' + ) + assert url == '' + + +class TestFormatTraceWithLinks: + """Tests for format_trace_with_links function.""" + + def test_empty_trace_lines(self): + result = github_helpers.format_trace_with_links( + [], + 'owner/repo', + 'abc123' + ) + assert result == '' + + def test_basic_trace_format(self): + trace_lines = [ + 'owasp-goat - server.js 72:12-75:6' + ] + + result = github_helpers.format_trace_with_links( + trace_lines, + 'owner/repo', + 'abc123', + enable_links=True + ) + + assert 'https://github.com/owner/repo' in result + assert '[server.js 72:12-75:6]' in result + assert 'owasp-goat' in result + + def test_indented_trace_format(self): + trace_lines = [ + ' -> express routes/auth.js 45:2' + ] + + result = github_helpers.format_trace_with_links( + trace_lines, + 'owner/repo', + 'abc123', + enable_links=True + ) + + assert 'https://github.com/owner/repo' in result + assert '[routes/auth.js 45:2]' in result + assert 'express' in result + assert ' ->' in result # Preserves indentation + + def test_multiple_trace_lines(self): + trace_lines = [ + 'owasp-goat - server.js 72:12-75:6', + ' -> express routes/auth.js 45:2' + ] + + result = github_helpers.format_trace_with_links( + trace_lines, + 'owner/repo', + 'abc123', + enable_links=True + ) + + lines = result.split('\n') + assert len(lines) == 2 + assert '[server.js 72:12-75:6]' in lines[0] + assert '[routes/auth.js 45:2]' in lines[1] + + def test_links_disabled(self): + trace_lines = [ + 'owasp-goat - server.js 72:12-75:6' + ] + + result = github_helpers.format_trace_with_links( + trace_lines, + 'owner/repo', + 'abc123', + enable_links=False + ) + + assert 'https://github.com' not in result + assert result == 'owasp-goat - server.js 72:12-75:6' + + def test_unparseable_line_preserved(self): + trace_lines = [ + 'some random text without proper format' + ] + + result = github_helpers.format_trace_with_links( + trace_lines, + 'owner/repo', + 'abc123', + enable_links=True + ) + + assert result == 'some random text without proper format' + + def test_single_line_number(self): + trace_lines = [ + 'package - file.js 42:5' + ] + + result = github_helpers.format_trace_with_links( + trace_lines, + 'owner/repo', + 'abc123', + enable_links=True + ) + + assert '[file.js 42:5]' in result + assert '#L42' in result + + def test_missing_repository(self): + trace_lines = [ + 'package - file.js 42:5' + ] + + result = github_helpers.format_trace_with_links( + trace_lines, + '', + 'abc123', + enable_links=True + ) + + # Should preserve original format if URL can't be built + assert result == 'package - file.js 42:5' + + +class TestExtractRuleName: + """Tests for extract_rule_name function.""" + + def test_from_props_rule(self): + alert = { + 'props': { + 'rule': 'CVE-2024-1234' + } + } + assert github_helpers.extract_rule_name(alert) == 'CVE-2024-1234' + + def test_from_props_rule_name(self): + alert = { + 'props': { + 'rule_name': 'GHSA-xxxx-yyyy-zzzz' + } + } + assert github_helpers.extract_rule_name(alert) == 'GHSA-xxxx-yyyy-zzzz' + + def test_from_props_ruleName_camelCase(self): + alert = { + 'props': { + 'ruleName': 'custom-rule' + } + } + assert github_helpers.extract_rule_name(alert) == 'custom-rule' + + def test_from_alert_rule(self): + alert = { + 'rule': 'some-rule' + } + assert github_helpers.extract_rule_name(alert) == 'some-rule' + + def test_from_alert_type(self): + alert = { + 'type': 'vulnerability' + } + assert github_helpers.extract_rule_name(alert) == 'vulnerability' + + def test_priority_order(self): + # props.rule should take priority over props.rule_name + alert = { + 'props': { + 'rule': 'high-priority', + 'rule_name': 'low-priority' + } + } + assert github_helpers.extract_rule_name(alert) == 'high-priority' + + def test_empty_alert(self): + alert = {} + assert github_helpers.extract_rule_name(alert) == '' + + def test_none_props(self): + alert = { + 'props': None + } + assert github_helpers.extract_rule_name(alert) == '' + + def test_empty_values(self): + alert = { + 'props': { + 'rule': '', + 'rule_name': '' + } + } + assert github_helpers.extract_rule_name(alert) == '' From 4d68b020a128c122a242f036b4f663e291898825 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 15:27:09 -0800 Subject: [PATCH 08/29] Update GHA manifest to include new params --- action.yml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/action.yml b/action.yml index 1c15b6f..926a54b 100644 --- a/action.yml +++ b/action.yml @@ -87,6 +87,15 @@ runs: INPUT_WEBHOOK_URL: ${{ inputs.webhook_url }} SOCKET_ADDITIONAL_PARAMS: ${{ inputs.socket_additional_params }} SOCKET_TIER_1_ENABLED: ${{ inputs.socket_tier_1_enabled }} + INPUT_PR_COMMENT_LINKS_ENABLED: ${{ inputs.pr_comment_links_enabled }} + INPUT_PR_COMMENT_COLLAPSE_ENABLED: ${{ inputs.pr_comment_collapse_enabled }} + INPUT_PR_COMMENT_COLLAPSE_NON_CRITICAL: ${{ inputs.pr_comment_collapse_non_critical }} + INPUT_PR_COMMENT_CODE_FENCING_ENABLED: ${{ inputs.pr_comment_code_fencing_enabled }} + INPUT_PR_COMMENT_SHOW_RULE_NAMES: ${{ inputs.pr_comment_show_rule_names }} + INPUT_PR_LABELS_ENABLED: ${{ inputs.pr_labels_enabled }} + INPUT_PR_LABEL_CRITICAL: ${{ inputs.pr_label_critical }} + INPUT_PR_LABEL_HIGH: ${{ inputs.pr_label_high }} + INPUT_PR_LABEL_MEDIUM: ${{ inputs.pr_label_medium }} inputs: workspace: @@ -405,6 +414,42 @@ inputs: description: "Generic webhook URL for WebhookNotifier" required: false default: "" + pr_comment_links_enabled: + description: "Enable clickable file/line links in PR comments" + required: false + default: "true" + pr_comment_collapse_enabled: + description: "Enable collapsible sections in PR comments" + required: false + default: "true" + pr_comment_collapse_non_critical: + description: "Auto-collapse non-critical findings (critical stays expanded)" + required: false + default: "true" + pr_comment_code_fencing_enabled: + description: "Enable language-aware code fencing for trace output" + required: false + default: "true" + pr_comment_show_rule_names: + description: "Show explicit rule names for each finding" + required: false + default: "true" + pr_labels_enabled: + description: "Add severity-based labels to PRs" + required: false + default: "true" + pr_label_critical: + description: "Label name for critical severity findings" + required: false + default: "security: critical" + pr_label_high: + description: "Label name for high severity findings" + required: false + default: "security: high" + pr_label_medium: + description: "Label name for medium severity findings" + required: false + default: "security: medium" branding: icon: "shield" From b8e8b3053170eac8484d7b66adf7b248cb7f2643 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 15:32:43 -0800 Subject: [PATCH 09/29] Update git ignore to handle new docs --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 66ccb20..7534bfd 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,11 @@ test.py *.cpython-312.pyc file_generator.py .env +# Allow only specific markdown files (documentation), ignore all others +# Negations must come BEFORE the ignore rule +!README.md +!docs/*.md +!tests/README.md *.md test_results local_tests/ From e0b289c0791dd38d4d41793cea73dfa78ba951c1 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 15:33:08 -0800 Subject: [PATCH 10/29] Update README with new PR comment functionality --- README.md | 252 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 251 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d8e6f3..e61a12c 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,133 @@ Socket Basics can also run locally or in other CI/CD environments: - Auto-enablement for container scanning when images or Dockerfiles are specified - Support for both standard and GitHub Actions `INPUT_*` environment variables +## ๐ŸŽจ Enhanced PR Comments + +Socket Basics delivers **beautifully formatted, actionable PR comments** with smart defaults that work out of the box: + +### What You Get (Enabled by Default) + +- ๐Ÿ”— **Clickable File Links** โ€” Jump directly to the vulnerable code in GitHub +- ๐Ÿ“‹ **Collapsible Sections** โ€” Critical findings auto-expand, others collapse for easy scanning +- ๐ŸŽจ **Syntax Highlighting** โ€” Language-aware code blocks for better readability +- ๐Ÿท๏ธ **Explicit Rule Names** โ€” Clear identification of which security rule was triggered +- ๐Ÿš€ **Quick Access Links** โ€” Full scan report link prominently displayed at the top +- ๐Ÿท๏ธ **Auto-Labels** โ€” PRs automatically tagged with severity-based labels (e.g., `security: critical`) + +### Using the Defaults + +**Zero configuration needed!** Just use the standard GitHub Actions setup: + +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} +``` + +All PR comment enhancements are **enabled by default** with sensible settings: +- Critical findings: **Auto-expanded** โœ… +- High/Medium/Low findings: **Collapsed** (click to expand) +- File links: **Clickable** with line numbers +- Code blocks: **Syntax highlighted** based on file type +- Labels: **`security: critical`**, **`security: high`**, **`security: medium`** + +### Customizing PR Comments + +Need different behavior? Every feature can be customized: + +#### Example: Disable Specific Features + +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} + # Customize PR comment behavior + pr_comment_links_enabled: 'false' # Disable clickable links + pr_comment_collapse_enabled: 'false' # Show all findings expanded + pr_labels_enabled: 'false' # Don't add labels to PRs +``` + +#### Example: Custom Label Names + +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} + # Use custom label names + pr_label_critical: 'socket: critical' # Instead of 'security: critical' + pr_label_high: 'socket: high' # Instead of 'security: high' + pr_label_medium: 'socket: medium' # Instead of 'security: medium' +``` + +#### Example: Show All Findings Expanded + +```yaml +- uses: SocketDev/socket-basics@1.0.26 + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} + # Keep collapsible sections but expand everything + pr_comment_collapse_enabled: 'true' + pr_comment_collapse_non_critical: 'false' # Don't collapse non-critical +``` + +### All PR Comment Configuration Options + +| Option | Default | Description | +|--------|---------|-------------| +| `pr_comment_links_enabled` | `true` | Enable clickable file/line links in PR comments | +| `pr_comment_collapse_enabled` | `true` | Enable collapsible sections in PR comments | +| `pr_comment_collapse_non_critical` | `true` | Auto-collapse non-critical findings (critical stays expanded) | +| `pr_comment_code_fencing_enabled` | `true` | Enable language-aware syntax highlighting | +| `pr_comment_show_rule_names` | `true` | Show explicit rule names for each finding | +| `pr_labels_enabled` | `true` | Add severity-based labels to PRs | +| `pr_label_critical` | `"security: critical"` | Label name for critical severity findings | +| `pr_label_high` | `"security: high"` | Label name for high severity findings | +| `pr_label_medium` | `"security: medium"` | Label name for medium severity findings | + +### CLI Usage + +These options are also available via CLI: + +```bash +socket-basics \ + --pr-comment-links \ + --pr-comment-collapse \ + --pr-labels \ + --pr-label-critical "custom: critical" \ + --workspace /path/to/repo +``` + +### Environment Variables + +Or via environment variables: + +```bash +export INPUT_PR_COMMENT_LINKS_ENABLED=true +export INPUT_PR_COMMENT_COLLAPSE_ENABLED=true +export INPUT_PR_LABELS_ENABLED=true +export INPUT_PR_LABEL_CRITICAL="security: critical" +socket-basics --workspace /path/to/repo +``` + +๐Ÿ“– **[Complete PR Comment Features Guide โ†’](docs/pr-comment-features.md)** + ## ๐Ÿ“– Documentation ### Getting Started - [GitHub Actions Integration](docs/github-action.md) โ€” Complete guide with workflow examples +- [PR Comment Features Guide](docs/pr-comment-features.md) โ€” Detailed guide to PR comment customization - [Pre-Commit Hook Setup](docs/pre-commit-hook.md) โ€” Two installation methods (Docker vs native) - [Local Docker Installation](docs/local-install-docker.md) โ€” Run with Docker, no tools to install - [Local Installation](docs/local-installation.md) โ€” Install Socket CLI, Trivy, and other tools natively @@ -257,8 +380,135 @@ We welcome contributions! To add new features: 1. **New Connectors:** Implement under `socket_basics/core/connector/` 2. **New Notifiers:** Implement under `socket_basics/core/notification/` 3. **Configuration:** Add entries to `socket_basics/connectors.yaml` or `socket_basics/notifications.yaml` -4. **Tests:** Add test cases to `app_tests/` +4. **Tests:** See [Testing](#-testing) section below + +## ๐Ÿงช Testing + +Socket Basics uses a two-tier testing strategy to ensure code quality and scanner accuracy. + +### Test Structure + +``` +socket-basics/ +โ”œโ”€โ”€ tests/ # Unit & integration tests (pytest) +โ”‚ โ””โ”€โ”€ test_*.py # Fast, isolated tests of functions/modules +โ””โ”€โ”€ app_tests/ # End-to-end test fixtures + โ”œโ”€โ”€ juice-shop/ # Node.js vulnerable app + โ”œโ”€โ”€ pygoat/ # Python vulnerable app + โ”œโ”€โ”€ NodeGoat/ # Node.js vulnerable app + โ”œโ”€โ”€ DVWA/ # PHP vulnerable app + โ””โ”€โ”€ ... # Other deliberately vulnerable apps +``` + +### Running Unit Tests + +**Quick test run:** +```bash +# Setup (first time only) +python3 -m venv venv +source venv/bin/activate +pip install -e ".[dev]" + +# Run all unit tests +pytest + +# Run specific test file +pytest tests/test_github_helpers.py + +# Run with verbose output +pytest -v + +# Run with coverage report +pytest --cov=socket_basics tests/ +``` + +**Characteristics:** +- โšก **Fast** โ€” Milliseconds to seconds +- ๐ŸŽฏ **Isolated** โ€” No external dependencies +- ๐Ÿ”„ **Frequent** โ€” Run on every commit +- โœ… **Validates** โ€” Code logic and functions + +### End-to-End Testing with Real Apps + +The `app_tests/` directory contains deliberately vulnerable applications (git submodules) for validating scanner accuracy. + +**Purpose:** +- Verify scanners detect **known vulnerabilities** +- Test against **real-world code patterns** +- Ensure **multi-language coverage** +- Validate **entire scan pipeline** + +**Run E2E tests:** +```bash +# Scan a vulnerable Node.js app +socket-basics --workspace app_tests/juice-shop \ + --javascript-sast-enabled \ + --secret-scanning-enabled + +# Scan a vulnerable Python app +socket-basics --workspace app_tests/pygoat \ + --python-sast-enabled \ + --secret-scanning-enabled + +# Compare results against known vulnerabilities +# (Manual verification of findings) +``` + +**Characteristics:** +- ๐Ÿข **Slow** โ€” Minutes per app +- ๐Ÿ“ฆ **Large** โ€” Git submodules with full apps +- ๐ŸŽฏ **Ground truth** โ€” Known vulnerabilities to detect +- โœ… **Validates** โ€” Scanner accuracy and coverage + +### Adding Tests +**Adding Unit Tests:** +```python +# tests/test_new_feature.py +import pytest +from socket_basics.module import new_function + +def test_new_feature(): + result = new_function(input_data) + assert result == expected_output +``` + +**Adding E2E Test Fixtures:** +```bash +# Add a new vulnerable app as a git submodule +cd app_tests/ +git submodule add https://github.com/org/vulnerable-app +git submodule update --init +``` + +### Test Best Practices + +**For contributors:** +1. โœ… Add unit tests for new functions/modules +2. โœ… Run `pytest` before committing +3. โœ… Validate changes against `app_tests/` fixtures +4. โœ… Keep unit tests fast (mock external dependencies) + +**For security researchers:** +1. ๐Ÿ” Use `app_tests/` to validate scanner accuracy +2. ๐Ÿ“Š Compare findings against CVE databases +3. ๐ŸŽฏ Add new vulnerable apps as needed +4. ๐Ÿ“ Document expected findings for regression testing + +### CI/CD Testing + +```yaml +# Example GitHub Actions workflow +- name: Run Unit Tests + run: | + pip install -e ".[dev]" + pytest tests/ --cov=socket_basics + +- name: Run E2E Tests (Selected) + run: | + # Run against specific vulnerable apps + socket-basics --workspace app_tests/pygoat --python +``` --- From 6d27b902cd3c4b704d7d601a6a7c445fc0e56df4 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 16:37:53 -0800 Subject: [PATCH 11/29] Update helper tests import --- tests/test_github_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_github_helpers.py b/tests/test_github_helpers.py index b9fb5fa..f4e7d72 100644 --- a/tests/test_github_helpers.py +++ b/tests/test_github_helpers.py @@ -1,7 +1,7 @@ """Unit tests for GitHub PR comment helper functions.""" import pytest -from socket_basics.core.connector.socket_tier1 import github_helpers +from socket_basics.core.notification import github_pr_helpers as github_helpers class TestDetectLanguageFromFilename: From c6a1e8dad8437bdff269b405249457ed02c3ff2b Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 16:38:38 -0800 Subject: [PATCH 12/29] Update all connectors to have PR comment feature parity --- .../core/connector/opengrep/github_pr.py | 114 +++++++++++++----- .../core/connector/socket_tier1/github_pr.py | 43 +++---- .../core/connector/trivy/github_pr.py | 41 ++++--- .../core/connector/trufflehog/github_pr.py | 40 ++++-- 4 files changed, 160 insertions(+), 78 deletions(-) diff --git a/socket_basics/core/connector/opengrep/github_pr.py b/socket_basics/core/connector/opengrep/github_pr.py index d523fab..9cbad2e 100644 --- a/socket_basics/core/connector/opengrep/github_pr.py +++ b/socket_basics/core/connector/opengrep/github_pr.py @@ -8,6 +8,7 @@ from typing import Dict, Any, List import logging import yaml +from socket_basics.core.notification import github_pr_helpers as helpers logger = logging.getLogger(__name__) @@ -27,11 +28,22 @@ def _get_github_pr_result_limit() -> int: def format_notifications(groups: Dict[str, List[Dict[str, Any]]], config=None) -> List[Dict[str, Any]]: """Format for GitHub PR comments - detailed with markdown formatting.""" tables = [] - + + # Get feature flags from config (using shared helper) + flags = helpers.get_feature_flags(config) + enable_links = flags['enable_links'] + enable_collapse = flags['enable_collapse'] + collapse_non_critical = flags['collapse_non_critical'] + enable_code_fencing = flags['enable_code_fencing'] + show_rule_names = flags['show_rule_names'] + repository = flags['repository'] + commit_hash = flags['commit_hash'] + full_scan_url = flags['full_scan_url'] + # Map subtypes to friendly display names subtype_names = { 'sast-python': 'Socket SAST Python', - 'sast-javascript': 'Socket SAST JavaScript', + 'sast-javascript': 'Socket SAST JavaScript', 'sast-golang': 'Socket SAST Go', 'sast-java': 'Socket SAST Java', 'sast-php': 'Socket SAST PHP', @@ -45,14 +57,10 @@ def format_notifications(groups: Dict[str, List[Dict[str, Any]]], config=None) - 'sast-swift': 'Socket SAST Swift', 'sast-rust': 'Socket SAST Rust', } - - severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3} - severity_emoji = { - 'critical': '๐Ÿ”ด', - 'high': '๐ŸŸ ', - 'medium': '๐ŸŸก', - 'low': 'โšช' - } + + # Use shared severity constants + severity_order = helpers.SEVERITY_ORDER + severity_emoji = helpers.SEVERITY_EMOJI for subtype, items in groups.items(): # Group findings by file path, then by rule within each file @@ -127,39 +135,82 @@ def format_notifications(groups: Dict[str, List[Dict[str, Any]]], config=None) - file_name = Path(file_path).name except Exception: file_name = file_path - - # File header - content_lines.append(f"#### `{file_path}`") - content_lines.append("") - + + # Calculate severity counts for this file (for collapsible summary) + file_severities = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0} + for rule_id, locations in file_groups[file_path].items(): + for loc in locations: + sev = loc['severity'] + if sev in file_severities: + file_severities[sev] += 1 + # Sort rules by severity within file rules_in_file = [] for rule_id, locations in file_groups[file_path].items(): # Get highest severity for this rule min_severity = min(severity_order.get(loc['severity'], 4) for loc in locations) rules_in_file.append((min_severity, rule_id, locations)) - + rules_in_file.sort(key=lambda x: x[0]) - + + # Build file section content + file_content_lines = [] + # Output each rule with its locations for _, rule_id, locations in rules_in_file: # Get severity from first location (they should all be same rule) rule_severity = locations[0]['severity'] emoji = severity_emoji.get(rule_severity, 'โšช') - - content_lines.append(f"**{rule_id}** ") - content_lines.append(f"{emoji} *{rule_severity.upper()}*") - content_lines.append("") - + + # Show rule name + if show_rule_names: + file_content_lines.append(f"{emoji} **{rule_id}**: *{rule_severity.upper()}*") + else: + file_content_lines.append(f"{emoji} *{rule_severity.upper()}*") + file_content_lines.append("") + # Output each location with code snippet for loc in locations: - content_lines.append(f"**Lines {loc['start_line']}:{loc['end_line']}**") + # Create clickable file location link + location_link = helpers.format_file_location_link( + file_path, + loc['start_line'], + loc['end_line'], + repository, + commit_hash, + enable_links + ) + file_content_lines.append(location_link) + if loc['code_snippet']: - # Format code snippet in code block - content_lines.append("```") - content_lines.append(loc['code_snippet']) - content_lines.append("```") - content_lines.append("") + # Format code snippet with language-aware fencing + code_block = helpers.format_code_block( + loc['code_snippet'], + filepath=file_path, + enable_fencing=enable_code_fencing + ) + file_content_lines.append(code_block) + file_content_lines.append("") + + file_content = '\n'.join(file_content_lines) + + # Add collapsible section or plain header + if enable_collapse: + # Determine if this should be auto-expanded + has_critical = file_severities['critical'] > 0 + auto_expand = has_critical and not collapse_non_critical + + collapsible = helpers.create_collapsible_section( + f"`{file_path}`", + file_content, + severity_counts=file_severities, + auto_expand=auto_expand + ) + content_lines.append(collapsible) + else: + content_lines.append(f"#### `{file_path}`") + content_lines.append("") + content_lines.append(file_content) content = '\n'.join(content_lines) @@ -174,11 +225,14 @@ def format_notifications(groups: Dict[str, List[Dict[str, Any]]], config=None) - title_parts.append(config.commit_hash[:8]) # Short hash title = " - ".join(title_parts) - + + # Add full scan link at top if available (using shared helper) + scan_link_section = helpers.format_scan_link_section(full_scan_url) + # Wrap content with HTML comment markers for section updates wrapped_content = f""" # {title} - +{scan_link_section} {content} """ diff --git a/socket_basics/core/connector/socket_tier1/github_pr.py b/socket_basics/core/connector/socket_tier1/github_pr.py index 19f0532..889aaf6 100644 --- a/socket_basics/core/connector/socket_tier1/github_pr.py +++ b/socket_basics/core/connector/socket_tier1/github_pr.py @@ -2,7 +2,7 @@ import re from typing import Dict, Any, List -from . import github_helpers +from socket_basics.core.notification import github_pr_helpers as helpers def _make_purl(comp: Dict[str, Any]) -> str: @@ -29,25 +29,20 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> """Format for GitHub PR comments - grouped by PURL and reachability.""" from collections import defaultdict - # Get feature flags from config - enable_links = config.get('pr_comment_links_enabled', True) if config else True - enable_collapse = config.get('pr_comment_collapse_enabled', True) if config else True - collapse_non_critical = config.get('pr_comment_collapse_non_critical', True) if config else True - enable_code_fencing = config.get('pr_comment_code_fencing_enabled', True) if config else True - show_rule_names = config.get('pr_comment_show_rule_names', True) if config else True + # Get feature flags from config (using shared helper) + flags = helpers.get_feature_flags(config) + enable_links = flags['enable_links'] + enable_collapse = flags['enable_collapse'] + collapse_non_critical = flags['collapse_non_critical'] + enable_code_fencing = flags['enable_code_fencing'] + show_rule_names = flags['show_rule_names'] + repository = flags['repository'] + commit_hash = flags['commit_hash'] + full_scan_url = flags['full_scan_url'] - # Get GitHub metadata for links - repository = config.repo if config else '' - commit_hash = config.commit_hash if config else '' - full_scan_url = config.get('full_scan_html_url') if config else None - - severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3} - severity_emoji = { - 'critical': '๐Ÿ”ด', - 'high': '๐ŸŸ ', - 'medium': '๐ŸŸก', - 'low': 'โšช' - } + # Use shared severity constants + severity_order = helpers.SEVERITY_ORDER + severity_emoji = helpers.SEVERITY_EMOJI # Group by PURL -> Reachability -> Findings purl_groups = defaultdict(lambda: {'reachable': [], 'unknown': [], 'error': [], 'unreachable': []}) @@ -178,7 +173,7 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> # Add clickable links if enable_links and repository and commit_hash: trace_lines = trace_str.split('\n') - trace_str = github_helpers.format_trace_with_links( + trace_str = helpers.format_trace_with_links( trace_lines, repository, commit_hash, enable_links ) @@ -189,7 +184,7 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> first_line = trace_str.split('\n')[0] if trace_str else '' match = re.search(r'([^\s]+\.[\w]+)', first_line) if match: - lang = github_helpers.detect_language_from_filename(match.group(1)) + lang = helpers.detect_language_from_filename(match.group(1)) content_lines.append(f"```{lang}") content_lines.append(trace_str) @@ -266,10 +261,8 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> # Content already includes summary and details sections summary_content = content - # Add full scan link at top if available - scan_link_section = '' - if full_scan_url: - scan_link_section = f"\n\n๐Ÿ”— **[View Full Socket Scan Report]({full_scan_url})**\n\n---\n" + # Add full scan link at top if available (using shared helper) + scan_link_section = helpers.format_scan_link_section(full_scan_url) # Wrap content with HTML comment markers for section updates wrapped_content = f""" diff --git a/socket_basics/core/connector/trivy/github_pr.py b/socket_basics/core/connector/trivy/github_pr.py index fcee974..ab8db37 100644 --- a/socket_basics/core/connector/trivy/github_pr.py +++ b/socket_basics/core/connector/trivy/github_pr.py @@ -7,16 +7,29 @@ from typing import Dict, Any, List from collections import defaultdict from .utils import logger, get_notifier_result_limit +from socket_basics.core.notification import github_pr_helpers as helpers -def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", scan_type: str = "image") -> List[Dict[str, Any]]: +def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", scan_type: str = "image", config=None) -> List[Dict[str, Any]]: """Format for GitHub PR comments - grouped format with markdown formatting. - + Args: mapping: Component mapping with alerts item_name: Name of the scanned item scan_type: Type of scan - 'vuln', 'image', or 'dockerfile' + config: Optional configuration object with feature flags """ + # Get feature flags from config (using shared helper) + flags = helpers.get_feature_flags(config) + enable_links = flags['enable_links'] + enable_collapse = flags['enable_collapse'] + collapse_non_critical = flags['collapse_non_critical'] + enable_code_fencing = flags['enable_code_fencing'] + show_rule_names = flags['show_rule_names'] + repository = flags['repository'] + commit_hash = flags['commit_hash'] + full_scan_url = flags['full_scan_url'] + # Group vulnerabilities by package and severity package_groups = defaultdict(lambda: defaultdict(set)) # Use set to avoid duplicates @@ -63,7 +76,8 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc # Create rows with proper formatting rows = [] - severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3} + severity_order = helpers.SEVERITY_ORDER + severity_emoji = helpers.SEVERITY_EMOJI if scan_type == 'dockerfile': # Dockerfile format: Rule ID | Severity | Message | Resolution @@ -208,14 +222,8 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc # Panel format for vulnerability scanning panels = [] for vuln in rows: - # Determine panel color based on severity - severity_icons = { - 'critical': '๐Ÿ”ด', - 'high': '๐ŸŸ ', - 'medium': '๐ŸŸก', - 'low': '๐ŸŸข' - } - icon = severity_icons.get(vuln['severity'], 'โšช') + # Use shared severity emoji + icon = severity_emoji.get(vuln['severity'], 'โšช') severity_label = vuln['severity'].upper() # Create expandable panel for each CVE @@ -272,10 +280,10 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc scanner_name = "Trivy Container" title = f"{title_base}: {item_name}" - + # Count total findings for summary total_findings = len(rows) - + # Add summary section with scanner findings summary_content = f"""## Summary @@ -286,11 +294,14 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc ## Details {content}""" - + + # Add full scan link at top if available (using shared helper) + scan_link_section = helpers.format_scan_link_section(full_scan_url) + # Wrap content with HTML comment markers for section updates wrapped_content = f""" # {title} - +{scan_link_section} {summary_content} """ diff --git a/socket_basics/core/connector/trufflehog/github_pr.py b/socket_basics/core/connector/trufflehog/github_pr.py index 959d807..6ab5126 100644 --- a/socket_basics/core/connector/trufflehog/github_pr.py +++ b/socket_basics/core/connector/trufflehog/github_pr.py @@ -5,11 +5,22 @@ """ from typing import Dict, Any, List +from socket_basics.core.notification import github_pr_helpers as helpers def format_notifications(mapping: Dict[str, Any], config=None) -> List[Dict[str, Any]]: """Format for GitHub PR comments - detailed with markdown formatting.""" - severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3} + # Get feature flags from config (using shared helper) + flags = helpers.get_feature_flags(config) + enable_links = flags['enable_links'] + repository = flags['repository'] + commit_hash = flags['commit_hash'] + full_scan_url = flags['full_scan_url'] + + # Use shared severity constants + severity_order = helpers.SEVERITY_ORDER + severity_emoji = helpers.SEVERITY_EMOJI + rows = [] for comp in mapping.values(): @@ -24,15 +35,25 @@ def format_notifications(mapping: Dict[str, Any], config=None) -> List[Dict[str, # Format with markdown for better GitHub display status = 'โœ… **VERIFIED**' if verified else 'โš ๏ธ *Unverified*' - file_display = f"`{file_path}`" - if line: - file_display += f":{line}" - + + # Create clickable file location link + line_num = int(line) if line and line.isdigit() else None + file_display = helpers.format_file_location_link( + file_path, + line_start=line_num, + repository=repository, + commit_hash=commit_hash, + enable_links=enable_links + ) + + # Add severity emoji + emoji = severity_emoji.get(severity, 'โšช') + rows.append(( severity_order.get(severity, 4), [ f"**{detector}**", - f"*{severity.upper()}*", + f"{emoji} *{severity.upper()}*", status, file_display, f"`{redacted}`" if redacted else '-' @@ -81,11 +102,14 @@ def format_notifications(mapping: Dict[str, Any], config=None) -> List[Dict[str, ## Details {content}""" - + + # Add full scan link at top if available (using shared helper) + scan_link_section = helpers.format_scan_link_section(full_scan_url) + # Wrap content with HTML comment markers for section updates wrapped_content = f""" # {title} - +{scan_link_section} {summary_content} """ From 4284d2b344459e6581d8cc72d18fe41be39b7778 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 16:39:12 -0800 Subject: [PATCH 13/29] Centralize all GitHub PR helper logic --- .../core/notification/github_pr_helpers.py | 440 ++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 socket_basics/core/notification/github_pr_helpers.py diff --git a/socket_basics/core/notification/github_pr_helpers.py b/socket_basics/core/notification/github_pr_helpers.py new file mode 100644 index 0000000..7cf4ccc --- /dev/null +++ b/socket_basics/core/notification/github_pr_helpers.py @@ -0,0 +1,440 @@ +"""Shared helper functions for GitHub PR comment formatting across all scanners. + +This module provides centralized utilities for: +- Clickable file/line links +- Collapsible sections with severity summaries +- Language-aware code fencing +- Rule name extraction +- Configuration-based feature flags + +These utilities are designed to work with any security scanner (SAST, SCA, secrets, containers). +""" + +import re +from typing import Dict, Any, Optional, List, Tuple +from pathlib import Path + + +# ============================================================================ +# Severity Constants (shared across all scanners) +# ============================================================================ + +SEVERITY_ORDER = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3} + +SEVERITY_EMOJI = { + 'critical': '๐Ÿ”ด', + 'high': '๐ŸŸ ', + 'medium': '๐ŸŸก', + 'low': 'โšช' +} + + +# ============================================================================ +# Configuration Helper +# ============================================================================ + +def get_feature_flags(config) -> Dict[str, Any]: + """Extract PR comment feature flags from config object. + + Args: + config: Configuration object (may be None) + + Returns: + Dictionary of feature flags with defaults + """ + if not config: + return { + 'enable_links': True, + 'enable_collapse': True, + 'collapse_non_critical': True, + 'enable_code_fencing': True, + 'show_rule_names': True, + 'repository': '', + 'commit_hash': '', + 'full_scan_url': None + } + + return { + 'enable_links': config.get('pr_comment_links_enabled', True), + 'enable_collapse': config.get('pr_comment_collapse_enabled', True), + 'collapse_non_critical': config.get('pr_comment_collapse_non_critical', True), + 'enable_code_fencing': config.get('pr_comment_code_fencing_enabled', True), + 'show_rule_names': config.get('pr_comment_show_rule_names', True), + 'repository': config.repo if hasattr(config, 'repo') else '', + 'commit_hash': config.commit_hash if hasattr(config, 'commit_hash') else '', + 'full_scan_url': config.get('full_scan_html_url') if config else None + } + + +# ============================================================================ +# Language Detection +# ============================================================================ + +def detect_language_from_filename(filename: str) -> str: + """Detect programming language from file extension. + + Args: + filename: File path or name + + Returns: + Markdown language identifier for code fencing + """ + ext_map = { + '.js': 'javascript', + '.jsx': 'javascript', + '.ts': 'typescript', + '.tsx': 'typescript', + '.py': 'python', + '.go': 'go', + '.java': 'java', + '.kt': 'kotlin', + '.scala': 'scala', + '.rb': 'ruby', + '.php': 'php', + '.cs': 'csharp', + '.cpp': 'cpp', + '.c': 'c', + '.h': 'c', + '.hpp': 'cpp', + '.swift': 'swift', + '.rs': 'rust', + '.ex': 'elixir', + '.exs': 'elixir', + '.erl': 'erlang', + '.sh': 'bash', + '.yaml': 'yaml', + '.yml': 'yaml', + '.json': 'json', + '.xml': 'xml', + '.html': 'html', + '.css': 'css', + '.sql': 'sql', + '.dockerfile': 'dockerfile', + '.Dockerfile': 'dockerfile', + } + + ext = Path(filename).suffix.lower() + + # Special case for Dockerfile (no extension) + if Path(filename).name.lower() in ['dockerfile', 'dockerfile.dev', 'dockerfile.prod']: + return 'dockerfile' + + return ext_map.get(ext, '') + + +# ============================================================================ +# GitHub URL Building +# ============================================================================ + +def build_github_file_url( + repository: str, + commit_hash: str, + filepath: str, + line_start: Optional[int] = None, + line_end: Optional[int] = None +) -> str: + """Build a GitHub URL to a specific file and line range. + + Args: + repository: GitHub repository (e.g., "owner/repo") + commit_hash: Git commit hash + filepath: Relative file path from repository root + line_start: Starting line number (optional) + line_end: Ending line number (optional) + + Returns: + GitHub URL string (empty if repository/commit missing) + """ + if not repository or not commit_hash: + return '' + + # Clean filepath (remove leading ./ or /) + clean_path = filepath.lstrip('./') + + # Base URL + url = f"https://github.com/{repository}/blob/{commit_hash}/{clean_path}" + + # Add line anchor + if line_start is not None: + if line_end is not None and line_end != line_start: + url += f"#L{line_start}-L{line_end}" + else: + url += f"#L{line_start}" + + return url + + +def format_file_location_link( + filepath: str, + line_start: Optional[int] = None, + line_end: Optional[int] = None, + repository: str = '', + commit_hash: str = '', + enable_links: bool = True +) -> str: + """Format a file location as plain text or clickable markdown link. + + Args: + filepath: File path + line_start: Starting line number + line_end: Ending line number + repository: GitHub repository + commit_hash: Git commit hash + enable_links: Whether to create clickable links + + Returns: + Formatted string (plain or markdown link) + """ + # Build display text + location_text = f"`{filepath}`" + if line_start is not None: + if line_end is not None and line_end != line_start: + location_text += f" **Lines {line_start}-{line_end}**" + else: + location_text += f" **Line {line_start}**" + + # Add link if enabled and metadata available + if enable_links and repository and commit_hash: + url = build_github_file_url(repository, commit_hash, filepath, line_start, line_end) + if url: + # Make the filepath clickable + if line_start is not None: + if line_end is not None and line_end != line_start: + return f"[`{filepath}` Lines {line_start}-{line_end}]({url})" + else: + return f"[`{filepath}` Line {line_start}]({url})" + else: + return f"[`{filepath}`]({url})" + + return location_text + + +# ============================================================================ +# Trace Formatting (for Socket Tier 1 reachability) +# ============================================================================ + +def format_trace_with_links( + trace_lines: List[str], + repository: str, + commit_hash: str, + enable_links: bool = True +) -> str: + """Format trace lines with clickable GitHub links. + + Args: + trace_lines: List of trace strings (format: "package - filename.ext 10:5-15:20") + repository: GitHub repository + commit_hash: Git commit hash + enable_links: Whether to create clickable links + + Returns: + Formatted trace string with optional links + """ + if not trace_lines: + return '' + + formatted_lines = [] + + for line in trace_lines: + if not enable_links: + formatted_lines.append(line) + continue + + # Parse trace format: "package_name - filename.js 72:12-75:6" + # or " -> module_name path/to/file.py 45:2" + # Note: package names can contain dashes, so we look for " - " (space-dash-space) as separator + + # Try format with " - " separator first + match = re.match( + r'^(\s*)(.+?)\s+-\s+([^\s]+)\s+(\d+):(\d+)(?:-(\d+):(\d+))?$', + line + ) + + if not match: + # Try format with "-> " prefix (no " - " separator) + match = re.match( + r'^(\s+->)\s+(.+?)\s+([^\s]+)\s+(\d+):(\d+)(?:-(\d+):(\d+))?$', + line + ) + + if match: + prefix = match.group(1) + package = match.group(2).strip() + filename = match.group(3) + line_start = int(match.group(4)) + col_start = match.group(5) + line_end = int(match.group(6)) if match.group(6) else line_start + col_end = match.group(7) + + # Build GitHub URL + github_url = build_github_file_url( + repository, commit_hash, filename, line_start, line_end + ) + + if github_url: + # Create markdown link + location = f"{line_start}:{col_start}" + if line_end != line_start or (col_end and col_end != col_start): + location += f"-{line_end}:{col_end}" + + # Format based on whether it has the " - " separator or "-> " prefix + if ' - ' in line or line.strip().startswith('->'): + # Preserve original structure + if '-> ' in prefix: + formatted = f"{prefix} {package} [{filename} {location}]({github_url})" + else: + formatted = f"{prefix}{package} - [{filename} {location}]({github_url})" + else: + formatted = f"{prefix}{package} - [{filename} {location}]({github_url})" + + formatted_lines.append(formatted) + else: + formatted_lines.append(line) + else: + # Couldn't parse, keep original + formatted_lines.append(line) + + return '\n'.join(formatted_lines) + + +# ============================================================================ +# Collapsible Sections +# ============================================================================ + +def build_severity_summary(severity_counts: Dict[str, int]) -> str: + """Build a severity summary string with emojis. + + Args: + severity_counts: Dictionary mapping severity levels to counts + + Returns: + Formatted severity summary (e.g., "๐Ÿ”ด Critical: 3 | ๐ŸŸ  High: 14") + """ + parts = [] + for severity in ['critical', 'high', 'medium', 'low']: + count = severity_counts.get(severity, 0) + if count > 0: + emoji = SEVERITY_EMOJI.get(severity, 'โšช') + parts.append(f"{emoji} {severity.capitalize()}: {count}") + + return " | ".join(parts) if parts else "No issues" + + +def create_collapsible_section( + title: str, + content: str, + severity_counts: Optional[Dict[str, int]] = None, + auto_expand: bool = False +) -> str: + """Create a collapsible
section with optional severity summary. + + Args: + title: Section title (will be bolded) + content: Section content (markdown) + severity_counts: Optional severity counts to show in summary + auto_expand: Whether to auto-expand the section + + Returns: + Markdown collapsible section + """ + summary_line = f"{title}" + + if severity_counts: + severity_summary = build_severity_summary(severity_counts) + summary_line += f" ({severity_summary})" + + open_attr = ' open' if auto_expand else '' + + return f""" +{summary_line} + +{content} + +
+ +""" + + +# ============================================================================ +# Code Fencing +# ============================================================================ + +def format_code_block( + code: str, + filepath: Optional[str] = None, + language: Optional[str] = None, + enable_fencing: bool = True +) -> str: + """Format code in a language-aware fenced code block. + + Args: + code: Code content + filepath: Optional file path to detect language from + language: Explicit language override + enable_fencing: Whether to add code fencing + + Returns: + Formatted code block + """ + if not code or not code.strip(): + return '' + + if not enable_fencing: + return code + + # Determine language + lang = language or '' + if not lang and filepath: + lang = detect_language_from_filename(filepath) + + return f"```{lang}\n{code}\n```" + + +# ============================================================================ +# Rule Name Extraction +# ============================================================================ + +def extract_rule_name(alert_data: Dict[str, Any]) -> str: + """Extract rule name from alert data. + + Args: + alert_data: Alert dictionary + + Returns: + Rule name or empty string + """ + # Check various possible sources + props = alert_data.get('props', {}) or {} + + # Priority order for rule names + rule_name = ( + props.get('rule') or + props.get('rule_name') or + props.get('ruleName') or + props.get('ruleId') or + alert_data.get('rule') or + alert_data.get('type') or + alert_data.get('title') or + '' + ) + + return str(rule_name) if rule_name else '' + + +# ============================================================================ +# Scan Link Formatting +# ============================================================================ + +def format_scan_link_section(full_scan_url: Optional[str]) -> str: + """Format the full scan report link section. + + Args: + full_scan_url: URL to full scan report + + Returns: + Formatted markdown section (empty if no URL) + """ + if not full_scan_url: + return '' + + return f"\n\n๐Ÿ”— **[View Full Socket Scan Report]({full_scan_url})**\n\n---\n" From 8088ed16cf807bfd48b467c6880d6c3c240566ae Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 16:39:31 -0800 Subject: [PATCH 14/29] Update README with new enhancements --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e61a12c..2dcc07b 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,15 @@ Socket Basics can also run locally or in other CI/CD environments: ## ๐ŸŽจ Enhanced PR Comments -Socket Basics delivers **beautifully formatted, actionable PR comments** with smart defaults that work out of the box: +Socket Basics delivers **beautifully formatted, actionable PR comments** with smart defaults that work out of the box. + +### Universal Enhancements + +These features work across **all scanner types** for a consistent experience: +- โœ… Socket Tier 1 (Reachability Analysis) +- โœ… SAST (OpenGrep/Semgrep) +- โœ… Container Scanning (Trivy) +- โœ… Secret Detection (TruffleHog) ### What You Get (Enabled by Default) From 047b0bc5894b6644ca11f0051fb9d5bbc6c8b4bf Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 16:57:44 -0800 Subject: [PATCH 15/29] Fix clickable links and auto-collapse logic --- socket_basics/core/connector/opengrep/__init__.py | 2 +- socket_basics/core/connector/opengrep/github_pr.py | 5 +++-- socket_basics/core/connector/trivy/trivy.py | 2 +- socket_basics/core/connector/trufflehog/__init__.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/socket_basics/core/connector/opengrep/__init__.py b/socket_basics/core/connector/opengrep/__init__.py index 07ccb70..8be357a 100644 --- a/socket_basics/core/connector/opengrep/__init__.py +++ b/socket_basics/core/connector/opengrep/__init__.py @@ -558,7 +558,7 @@ def generate_notifications(self, components: List[Dict[str, Any]]) -> Dict[str, # Build notifications for each notifier type using OpenGrep-specific modules notifications_by_notifier = {} - notifications_by_notifier['github_pr'] = github_pr.format_notifications(groups) + notifications_by_notifier['github_pr'] = github_pr.format_notifications(groups, config=self.config) notifications_by_notifier['slack'] = slack.format_notifications(groups) notifications_by_notifier['msteams'] = ms_teams.format_notifications(groups) notifications_by_notifier['ms_sentinel'] = ms_sentinel.format_notifications(groups) diff --git a/socket_basics/core/connector/opengrep/github_pr.py b/socket_basics/core/connector/opengrep/github_pr.py index 9cbad2e..26b5ccd 100644 --- a/socket_basics/core/connector/opengrep/github_pr.py +++ b/socket_basics/core/connector/opengrep/github_pr.py @@ -198,10 +198,11 @@ def format_notifications(groups: Dict[str, List[Dict[str, Any]]], config=None) - if enable_collapse: # Determine if this should be auto-expanded has_critical = file_severities['critical'] > 0 - auto_expand = has_critical and not collapse_non_critical + # Auto-expand if: no collapse requested OR has critical findings + auto_expand = (not collapse_non_critical) or has_critical collapsible = helpers.create_collapsible_section( - f"`{file_path}`", + file_path, # Don't use backticks in summary - they don't render in GitHub file_content, severity_counts=file_severities, auto_expand=auto_expand diff --git a/socket_basics/core/connector/trivy/trivy.py b/socket_basics/core/connector/trivy/trivy.py index a80af01..c4f518b 100644 --- a/socket_basics/core/connector/trivy/trivy.py +++ b/socket_basics/core/connector/trivy/trivy.py @@ -1082,7 +1082,7 @@ def generate_notifications(self, components: List[Dict[str, Any]], item_name: st # Build notifications for each notifier type using Trivy-specific modules notifications_by_notifier = {} - notifications_by_notifier['github_pr'] = github_pr.format_notifications(comps_map, item_name, scan_type) + notifications_by_notifier['github_pr'] = github_pr.format_notifications(comps_map, item_name, scan_type, config=self.config) notifications_by_notifier['slack'] = slack.format_notifications(comps_map, item_name, scan_type) notifications_by_notifier['msteams'] = ms_teams.format_notifications(comps_map, item_name, scan_type) notifications_by_notifier['ms_sentinel'] = ms_sentinel.format_notifications(comps_map, item_name, scan_type) diff --git a/socket_basics/core/connector/trufflehog/__init__.py b/socket_basics/core/connector/trufflehog/__init__.py index 9cba07d..4ff6ccf 100644 --- a/socket_basics/core/connector/trufflehog/__init__.py +++ b/socket_basics/core/connector/trufflehog/__init__.py @@ -461,7 +461,7 @@ def generate_notifications(self, components: List[Dict[str, Any]]) -> Dict[str, # Build notifications for each notifier type using TruffleHog-specific modules notifications_by_notifier = {} - notifications_by_notifier['github_pr'] = github_pr.format_notifications(filtered_comps_map) + notifications_by_notifier['github_pr'] = github_pr.format_notifications(filtered_comps_map, config=self.config) notifications_by_notifier['slack'] = slack.format_notifications(filtered_comps_map) notifications_by_notifier['msteams'] = ms_teams.format_notifications(filtered_comps_map) notifications_by_notifier['ms_sentinel'] = ms_sentinel.format_notifications(filtered_comps_map) From f3a7ae31c5821ccbdad9172d0263ef05d07609b1 Mon Sep 17 00:00:00 2001 From: lelia Date: Thu, 5 Feb 2026 17:22:19 -0800 Subject: [PATCH 16/29] Prune GHA workspace path prefixes --- .../core/notification/github_pr_helpers.py | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/socket_basics/core/notification/github_pr_helpers.py b/socket_basics/core/notification/github_pr_helpers.py index 7cf4ccc..7b8b13e 100644 --- a/socket_basics/core/notification/github_pr_helpers.py +++ b/socket_basics/core/notification/github_pr_helpers.py @@ -148,8 +148,31 @@ def build_github_file_url( if not repository or not commit_hash: return '' - # Clean filepath (remove leading ./ or /) - clean_path = filepath.lstrip('./') + # Clean filepath - remove GitHub Actions workspace prefixes + clean_path = filepath + + # Strip common GitHub Actions workspace prefixes + workspace_prefixes = [ + '/github/workspace/', + 'github/workspace/', + '/home/runner/work/', + ] + + for prefix in workspace_prefixes: + if clean_path.startswith(prefix): + clean_path = clean_path[len(prefix):] + break + + # For /home/runner/work/{owner}/{repo}/ pattern, strip the repo path + # Pattern: /home/runner/work/owner/repo/path/to/file.js + if clean_path.startswith('/home/runner/work/') or clean_path.startswith('home/runner/work/'): + parts = clean_path.split('/') + # Find where the actual file path starts (after owner/repo) + if len(parts) > 4: # ['', 'home', 'runner', 'work', 'owner', 'repo', ...] + clean_path = '/'.join(parts[6:]) if parts[0] == '' else '/'.join(parts[5:]) + + # Remove leading ./ or / + clean_path = clean_path.lstrip('./') # Base URL url = f"https://github.com/{repository}/blob/{commit_hash}/{clean_path}" From 15849f0a68edfa907c441d73da1384e457f8844f Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 16:26:46 -0800 Subject: [PATCH 17/29] Update connector formatting Signed-off-by: lelia --- .../core/connector/opengrep/github_pr.py | 16 +--- .../core/connector/socket_tier1/github_pr.py | 81 ++++++++++++------- .../core/connector/trivy/github_pr.py | 63 ++++++++++----- .../core/connector/trufflehog/github_pr.py | 24 ++---- 4 files changed, 104 insertions(+), 80 deletions(-) diff --git a/socket_basics/core/connector/opengrep/github_pr.py b/socket_basics/core/connector/opengrep/github_pr.py index 26b5ccd..98b45f1 100644 --- a/socket_basics/core/connector/opengrep/github_pr.py +++ b/socket_basics/core/connector/opengrep/github_pr.py @@ -215,24 +215,16 @@ def format_notifications(groups: Dict[str, List[Dict[str, Any]]], config=None) - content = '\n'.join(content_lines) - # Build title - title_parts = [display_name] - if config: - if config.repo: - title_parts.append(config.repo) - if config.branch: - title_parts.append(config.branch) - if config.commit_hash: - title_parts.append(config.commit_hash[:8]) # Short hash - - title = " - ".join(title_parts) + # Build title - just scanner name (repo/branch context already visible in PR) + title = display_name # Add full scan link at top if available (using shared helper) scan_link_section = helpers.format_scan_link_section(full_scan_url) # Wrap content with HTML comment markers for section updates wrapped_content = f""" -# {title} +## {title} + {scan_link_section} {content} """ diff --git a/socket_basics/core/connector/socket_tier1/github_pr.py b/socket_basics/core/connector/socket_tier1/github_pr.py index 889aaf6..975c2a7 100644 --- a/socket_basics/core/connector/socket_tier1/github_pr.py +++ b/socket_basics/core/connector/socket_tier1/github_pr.py @@ -57,11 +57,19 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> 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 '').lower() reachability = str(props.get('reachability') or 'unknown').lower() - + + # Get CVSS score if available + cvss_score = None + if 'cvssScore' in props: + try: + cvss_score = float(props['cvssScore']) + except (ValueError, TypeError): + pass + # Count by severity if severity in severity_counts: severity_counts[severity] += 1 - + # Get trace data trace_raw = props.get('trace') or '' trace_str = '' @@ -69,17 +77,18 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> trace_str = '\n'.join(str(x) for x in trace_raw) elif isinstance(trace_raw, str): trace_str = trace_raw - + # Truncate long traces if trace_str and len(trace_str) > 500: trace_str = trace_str[:500] + '\n...' - + finding = { 'cve_id': cve_id, 'severity': severity, 'severity_order': severity_order.get(severity, 4), 'trace': trace_str, - 'rule_name': a.get('title') or cve_id # Add rule name + 'rule_name': a.get('title') or cve_id, + 'cvss_score': cvss_score } # Group by reachability @@ -158,10 +167,15 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> content_lines.append("**Reachable**") content_lines.append("") for finding in purl_groups[purl]['reachable']: - emoji = severity_emoji.get(finding['severity'], 'โšช') - content_lines.append(f"{emoji} **{finding['cve_id']}**: *{finding['severity'].upper()}*") + # Use new vulnerability header format with CVE link and CVSS score + header = helpers.format_vulnerability_header( + finding['cve_id'], + finding['severity'], + finding.get('cvss_score') + ) + content_lines.append(header) - # Add rule name if enabled + # Add rule name if enabled and different from CVE ID if show_rule_names: rule_name = finding.get('rule_name', '') if rule_name and rule_name != finding['cve_id']: @@ -196,10 +210,15 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> content_lines.append("**Unknown**") content_lines.append("") for finding in purl_groups[purl]['unknown']: - emoji = severity_emoji.get(finding['severity'], 'โšช') - content_lines.append(f"{emoji} **{finding['cve_id']}**: *{finding['severity'].upper()}*") + # Use new vulnerability header format + header = helpers.format_vulnerability_header( + finding['cve_id'], + finding['severity'], + finding.get('cvss_score') + ) + content_lines.append(header) - # Add rule name if enabled + # Add rule name if enabled and different from CVE ID if show_rule_names: rule_name = finding.get('rule_name', '') if rule_name and rule_name != finding['cve_id']: @@ -211,10 +230,15 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> content_lines.append("**Error**") content_lines.append("") for finding in purl_groups[purl]['error']: - emoji = severity_emoji.get(finding['severity'], 'โšช') - content_lines.append(f"{emoji} **{finding['cve_id']}**: *{finding['severity'].upper()}*") + # Use new vulnerability header format + header = helpers.format_vulnerability_header( + finding['cve_id'], + finding['severity'], + finding.get('cvss_score') + ) + content_lines.append(header) - # Add rule name if enabled + # Add rule name if enabled and different from CVE ID if show_rule_names: rule_name = finding.get('rule_name', '') if rule_name and rule_name != finding['cve_id']: @@ -226,10 +250,15 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> content_lines.append("**Unreachable**") content_lines.append("") for finding in purl_groups[purl]['unreachable']: - emoji = severity_emoji.get(finding['severity'], 'โšช') - content_lines.append(f"{emoji} **{finding['cve_id']}**: *{finding['severity'].upper()}*") + # Use new vulnerability header format + header = helpers.format_vulnerability_header( + finding['cve_id'], + finding['severity'], + finding.get('cvss_score') + ) + content_lines.append(header) - # Add rule name if enabled + # Add rule name if enabled and different from CVE ID if show_rule_names: rule_name = finding.get('rule_name', '') if rule_name and rule_name != finding['cve_id']: @@ -243,18 +272,9 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> content = '\n'.join(content_lines) - # Build title with repo/branch/commit info from config - title_parts = ["Socket Security Tier 1 Results"] - if config: - if config.repo: - title_parts.append(config.repo) - if config.branch: - title_parts.append(config.branch) - if config.commit_hash: - title_parts.append(config.commit_hash) - - title = " - ".join(title_parts) - + # Build title - just scanner name (repo/branch context already visible in PR) + title = "Socket Security Tier 1" + # Count total findings total_findings = sum(severity_counts.values()) @@ -266,7 +286,8 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> # Wrap content with HTML comment markers for section updates wrapped_content = f""" -# {title} +## {title} + {scan_link_section} {summary_content} """ diff --git a/socket_basics/core/connector/trivy/github_pr.py b/socket_basics/core/connector/trivy/github_pr.py index ab8db37..000c14e 100644 --- a/socket_basics/core/connector/trivy/github_pr.py +++ b/socket_basics/core/connector/trivy/github_pr.py @@ -132,17 +132,25 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc cve_id = str(props.get('vulnerabilityId', '')) severity = str(alert.get('severity', '')).lower() description = str(alert.get('description', 'No description available')) - + + # Get CVSS score if available + cvss_score = None + if 'cvssScore' in props: + try: + cvss_score = float(props['cvssScore']) + except (ValueError, TypeError): + pass + # Build package identifier if comp_version: package = f"pkg:{ecosystem}/{comp_name}@{comp_version}" else: package = f"pkg:{ecosystem}/{comp_name}" - + # Get additional metadata fixed_version = str(props.get('fixedVersion', 'Not available')) installed_version = comp_version or 'Unknown' - + vuln_details.append({ 'cve_id': cve_id, 'severity': severity, @@ -152,7 +160,8 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc 'ecosystem': ecosystem, 'installed_version': installed_version, 'fixed_version': fixed_version, - 'description': description + 'description': description, + 'cvss_score': cvss_score }) # Sort by severity @@ -174,17 +183,25 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc cve_id = str(props.get('vulnerabilityId', '')) severity = str(alert.get('severity', '')).lower() description = str(alert.get('description', 'No description available')) - + + # Get CVSS score if available + cvss_score = None + if 'cvssScore' in props: + try: + cvss_score = float(props['cvssScore']) + except (ValueError, TypeError): + pass + # Build package identifier if comp_version: package = f"pkg:{ecosystem}/{comp_name}@{comp_version}" else: package = f"pkg:{ecosystem}/{comp_name}" - + # Get additional metadata fixed_version = str(props.get('fixedVersion', 'Not available')) installed_version = comp_version or 'Unknown' - + vuln_details.append({ 'cve_id': cve_id, 'severity': severity, @@ -194,7 +211,8 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc 'ecosystem': ecosystem, 'installed_version': installed_version, 'fixed_version': fixed_version, - 'description': description + 'description': description, + 'cvss_score': cvss_score }) # Sort by severity @@ -222,15 +240,17 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc # Panel format for vulnerability scanning panels = [] for vuln in rows: - # Use shared severity emoji - icon = severity_emoji.get(vuln['severity'], 'โšช') - severity_label = vuln['severity'].upper() - + # Create vulnerability header with CVE link and CVSS score + vuln_header = helpers.format_vulnerability_header( + vuln['cve_id'], + vuln['severity'], + vuln.get('cvss_score') + ) + # Create expandable panel for each CVE panel = f"""
-{icon} {vuln['cve_id']} +{vuln_header} -**Severity:** {severity_label} **Package:** `{vuln['package']}` @@ -270,28 +290,26 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc # Build title based on scan type if scan_type == 'vuln': - title_base = "Socket CVE Scanning Results" + title = "Socket CVE Scanning" scanner_name = "Trivy Vuln Scanning" elif scan_type == 'dockerfile': - title_base = "Socket Dockerfile Results" + title = "Socket Dockerfile Scan" scanner_name = "Trivy Dockerfile" else: # image - title_base = "Socket Image Scanning Results" + title = "Socket Container Scan" scanner_name = "Trivy Container" - - title = f"{title_base}: {item_name}" # Count total findings for summary total_findings = len(rows) # Add summary section with scanner findings - summary_content = f"""## Summary + summary_content = f"""### Summary | Scanner | Findings | |---------|----------| | {scanner_name} | {total_findings} | -## Details +### Details {content}""" @@ -300,7 +318,8 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc # Wrap content with HTML comment markers for section updates wrapped_content = f""" -# {title} +## {title} + {scan_link_section} {summary_content} """ diff --git a/socket_basics/core/connector/trufflehog/github_pr.py b/socket_basics/core/connector/trufflehog/github_pr.py index 6ab5126..23aa7f9 100644 --- a/socket_basics/core/connector/trufflehog/github_pr.py +++ b/socket_basics/core/connector/trufflehog/github_pr.py @@ -77,29 +77,20 @@ def format_notifications(mapping: Dict[str, Any], config=None) -> List[Dict[str, content = '\n'.join([header_row, separator_row] + content_rows) - # Build title with repo/branch/commit info from config - title_parts = ["Socket Security Results"] - if config: - if config.repo: - title_parts.append(config.repo) - if config.branch: - title_parts.append(config.branch) - if config.commit_hash: - title_parts.append(config.commit_hash) - - title = " - ".join(title_parts) - + # Build title - just scanner name (repo/branch context already visible in PR) + title = "Socket Secret Scanning" + # Count total findings for summary total_findings = len(rows) - + # Add summary section with scanner findings - summary_content = f"""## Summary + summary_content = f"""### Summary | Scanner | Findings | |---------|----------| | TruffleHog Secrets | {total_findings} | -## Details +### Details {content}""" @@ -108,7 +99,8 @@ def format_notifications(mapping: Dict[str, Any], config=None) -> List[Dict[str, # Wrap content with HTML comment markers for section updates wrapped_content = f""" -# {title} +## {title} + {scan_link_section} {summary_content} """ From fdb667bf3af259e7f9014a0acfb71180aaa04b7a Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 16:27:06 -0800 Subject: [PATCH 18/29] Colorize GitHub PR label options Signed-off-by: lelia --- .../core/notification/github_pr_notifier.py | 87 ++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/socket_basics/core/notification/github_pr_notifier.py b/socket_basics/core/notification/github_pr_notifier.py index e3d07fe..555d03e 100644 --- a/socket_basics/core/notification/github_pr_notifier.py +++ b/socket_basics/core/notification/github_pr_notifier.py @@ -377,8 +377,65 @@ def _post_comment(self, pr_number: int, comment_body: str) -> bool: logger.error('GithubPRNotifier: exception posting comment: %s', e) return False + def _ensure_label_exists_with_color(self, label_name: str, color: str, description: str = '') -> bool: + """Ensure a label exists in the repository with the specified color. + + If the label doesn't exist, it will be created with the given color. + If it already exists, we leave it alone (don't update existing labels). + + Args: + label_name: Name of the label + color: Hex color code (without #), e.g., 'D73A4A' + description: Optional description for the label + + Returns: + True if label exists/was created, False otherwise + """ + if not self.repository: + return False + + try: + import requests + headers = { + 'Authorization': f'token {self.token}', + 'Accept': 'application/vnd.github.v3+json' + } + + # Check if label exists + check_url = f"{self.api_base}/repos/{self.repository}/labels/{label_name}" + resp = requests.get(check_url, headers=headers, timeout=10) + + if resp.status_code == 200: + # Label already exists, don't modify it + logger.debug('GithubPRNotifier: label "%s" already exists', label_name) + return True + elif resp.status_code == 404: + # Label doesn't exist, create it + create_url = f"{self.api_base}/repos/{self.repository}/labels" + payload = { + 'name': label_name, + 'color': color, + 'description': description + } + + create_resp = requests.post(create_url, headers=headers, json=payload, timeout=10) + if create_resp.status_code == 201: + logger.info('GithubPRNotifier: created label "%s" with color #%s', label_name, color) + return True + else: + logger.warning('GithubPRNotifier: failed to create label "%s": %s', + label_name, create_resp.status_code) + return False + else: + logger.warning('GithubPRNotifier: unexpected response checking label: %s', resp.status_code) + return False + + except Exception as e: + logger.debug('GithubPRNotifier: exception ensuring label exists: %s', e) + return False + def _add_pr_labels(self, pr_number: int, labels: List[str]) -> bool: - """Add labels to a PR. + """Add labels to a PR, ensuring they exist with appropriate colors. Args: pr_number: PR number @@ -390,6 +447,34 @@ def _add_pr_labels(self, pr_number: int, labels: List[str]) -> bool: if not self.repository or not labels: return False + # Color mapping for severity labels (matching emoji colors) + label_colors = { + 'security: critical': ('D73A4A', 'Critical security vulnerabilities'), + 'security: high': ('D93F0B', 'High severity security issues'), + 'security: medium': ('FBCA04', 'Medium severity security issues'), + 'security: low': ('E4E4E4', 'Low severity security issues'), + } + + # Ensure labels exist with correct colors + for label in labels: + # Get color and description if this is a known severity label + color_info = label_colors.get(label) + if color_info: + color, description = color_info + self._ensure_label_exists_with_color(label, color, description) + # For custom label names, use a default color + elif ':' in label: + # Try to infer severity from label name + label_lower = label.lower() + if 'critical' in label_lower: + self._ensure_label_exists_with_color(label, 'D73A4A', 'Critical security vulnerabilities') + elif 'high' in label_lower: + self._ensure_label_exists_with_color(label, 'D93F0B', 'High severity security issues') + elif 'medium' in label_lower: + self._ensure_label_exists_with_color(label, 'FBCA04', 'Medium severity security issues') + elif 'low' in label_lower: + self._ensure_label_exists_with_color(label, 'E4E4E4', 'Low severity security issues') + try: import requests headers = { From 8ffefd9fc7fceebee25305b6bc566f31d2ea841f Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 16:27:41 -0800 Subject: [PATCH 19/29] Better filepath formatting, CVE links, CVSS scores Signed-off-by: lelia --- .../core/notification/github_pr_helpers.py | 77 +++++++++++++++++-- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/socket_basics/core/notification/github_pr_helpers.py b/socket_basics/core/notification/github_pr_helpers.py index 7b8b13e..27313b8 100644 --- a/socket_basics/core/notification/github_pr_helpers.py +++ b/socket_basics/core/notification/github_pr_helpers.py @@ -197,6 +197,8 @@ def format_file_location_link( ) -> str: """Format a file location as plain text or clickable markdown link. + Uses common file:line convention (e.g., "file.js:72" or "file.js:72-85") + Args: filepath: File path line_start: Starting line number @@ -208,26 +210,27 @@ def format_file_location_link( Returns: Formatted string (plain or markdown link) """ - # Build display text - location_text = f"`{filepath}`" + # Build display text using file:line convention if line_start is not None: if line_end is not None and line_end != line_start: - location_text += f" **Lines {line_start}-{line_end}**" + location_text = f"`{filepath}:{line_start}-{line_end}`" else: - location_text += f" **Line {line_start}**" + location_text = f"`{filepath}:{line_start}`" + else: + location_text = f"`{filepath}`" # Add link if enabled and metadata available if enable_links and repository and commit_hash: url = build_github_file_url(repository, commit_hash, filepath, line_start, line_end) if url: - # Make the filepath clickable + # Make the entire location clickable (no backticks inside link text) if line_start is not None: if line_end is not None and line_end != line_start: - return f"[`{filepath}` Lines {line_start}-{line_end}]({url})" + return f"[{filepath}:{line_start}-{line_end}]({url})" else: - return f"[`{filepath}` Line {line_start}]({url})" + return f"[{filepath}:{line_start}]({url})" else: - return f"[`{filepath}`]({url})" + return f"[{filepath}]({url})" return location_text @@ -461,3 +464,61 @@ def format_scan_link_section(full_scan_url: Optional[str]) -> str: return '' return f"\n\n๐Ÿ”— **[View Full Socket Scan Report]({full_scan_url})**\n\n---\n" + + +# ============================================================================ +# CVE/Vulnerability Formatting +# ============================================================================ + +def format_cve_link(cve_id: str) -> str: + """Format a CVE ID as a clickable link to NVD. + + Args: + cve_id: CVE identifier (e.g., "CVE-2021-23337") + + Returns: + Markdown link to NVD, or plain text if not a valid CVE ID + """ + if not cve_id: + return '' + + # Check if it's a CVE ID (format: CVE-YYYY-NNNNN) + if cve_id.upper().startswith('CVE-'): + return f"[{cve_id}](https://nvd.nist.gov/vuln/detail/{cve_id})" + + # Not a CVE, return as-is + return cve_id + + +def format_vulnerability_header( + vuln_id: str, + severity: str, + cvss_score: Optional[float] = None, + emoji: Optional[str] = None +) -> str: + """Format a vulnerability header with severity and optional CVSS score. + + Args: + vuln_id: Vulnerability ID (CVE, GHSA, etc.) + severity: Severity level (critical, high, medium, low) + cvss_score: Optional CVSS score (0.0-10.0) + emoji: Optional severity emoji (will use SEVERITY_EMOJI if not provided) + + Returns: + Formatted header string + """ + # Get emoji if not provided + if not emoji: + emoji = SEVERITY_EMOJI.get(severity.lower(), 'โšช') + + # Format CVE as link if applicable + vuln_display = format_cve_link(vuln_id) if vuln_id else 'Unknown' + + # Build header + header = f"{emoji} **{vuln_display}** โ€ข {severity.upper()}" + + # Add CVSS score if available + if cvss_score is not None: + header += f" (CVSS {cvss_score})" + + return header From 64552eb564b61be7d6c7bbf2959f6208f1e6a945 Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 16:27:56 -0800 Subject: [PATCH 20/29] Update helper tests to cover new functionality Signed-off-by: lelia --- tests/test_github_helpers.py | 99 ++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/tests/test_github_helpers.py b/tests/test_github_helpers.py index f4e7d72..227a1b7 100644 --- a/tests/test_github_helpers.py +++ b/tests/test_github_helpers.py @@ -327,3 +327,102 @@ def test_empty_values(self): } } assert github_helpers.extract_rule_name(alert) == '' + + +class TestFormatCveLink: + """Tests for format_cve_link function.""" + + def test_valid_cve_id(self): + result = github_helpers.format_cve_link('CVE-2021-23337') + assert result == '[CVE-2021-23337](https://nvd.nist.gov/vuln/detail/CVE-2021-23337)' + + def test_cve_id_lowercase(self): + result = github_helpers.format_cve_link('cve-2021-44228') + assert result == '[cve-2021-44228](https://nvd.nist.gov/vuln/detail/cve-2021-44228)' + + def test_non_cve_id(self): + result = github_helpers.format_cve_link('GHSA-abcd-1234-efgh') + assert result == 'GHSA-abcd-1234-efgh' + + def test_empty_string(self): + result = github_helpers.format_cve_link('') + assert result == '' + + def test_none_value(self): + result = github_helpers.format_cve_link(None) + assert result == '' + + +class TestFormatVulnerabilityHeader: + """Tests for format_vulnerability_header function.""" + + def test_with_cvss_score(self): + result = github_helpers.format_vulnerability_header( + 'CVE-2021-23337', + 'critical', + cvss_score=9.8 + ) + assert '๐Ÿ”ด' in result + assert '[CVE-2021-23337](https://nvd.nist.gov/vuln/detail/CVE-2021-23337)' in result + assert 'CRITICAL' in result + assert 'CVSS 9.8' in result + + def test_without_cvss_score(self): + result = github_helpers.format_vulnerability_header( + 'CVE-2021-23338', + 'high' + ) + assert '๐ŸŸ ' in result + assert '[CVE-2021-23338](https://nvd.nist.gov/vuln/detail/CVE-2021-23338)' in result + assert 'HIGH' in result + assert 'CVSS' not in result + + def test_with_custom_emoji(self): + result = github_helpers.format_vulnerability_header( + 'CVE-2021-23337', + 'critical', + cvss_score=9.8, + emoji='โš ๏ธ' + ) + assert 'โš ๏ธ' in result + assert '๐Ÿ”ด' not in result + + def test_non_cve_vulnerability(self): + result = github_helpers.format_vulnerability_header( + 'GHSA-abcd-1234-efgh', + 'high', + cvss_score=7.5 + ) + assert '๐ŸŸ ' in result + assert 'GHSA-abcd-1234-efgh' in result + assert 'https://nvd.nist.gov' not in result + assert 'HIGH' in result + assert 'CVSS 7.5' in result + + def test_medium_severity(self): + result = github_helpers.format_vulnerability_header( + 'CVE-2021-12345', + 'medium', + cvss_score=5.0 + ) + assert '๐ŸŸก' in result + assert 'MEDIUM' in result + assert 'CVSS 5.0' in result + + def test_low_severity(self): + result = github_helpers.format_vulnerability_header( + 'CVE-2021-67890', + 'low' + ) + assert 'โšช' in result + assert 'LOW' in result + + def test_empty_vuln_id(self): + result = github_helpers.format_vulnerability_header( + '', + 'critical', + cvss_score=10.0 + ) + assert 'Unknown' in result + assert 'CRITICAL' in result + assert 'CVSS 10.0' in result From 100e2e6ade39857c372975f26d6861ee4af8b8b2 Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 17:00:56 -0800 Subject: [PATCH 21/29] Wrap standard connector logic Signed-off-by: lelia --- socket_basics/core/connector/opengrep/github_pr.py | 14 ++++---------- .../core/connector/socket_tier1/github_pr.py | 14 ++++---------- socket_basics/core/connector/trivy/github_pr.py | 14 ++++---------- .../core/connector/trufflehog/github_pr.py | 14 ++++---------- 4 files changed, 16 insertions(+), 40 deletions(-) diff --git a/socket_basics/core/connector/opengrep/github_pr.py b/socket_basics/core/connector/opengrep/github_pr.py index 98b45f1..be3c247 100644 --- a/socket_basics/core/connector/opengrep/github_pr.py +++ b/socket_basics/core/connector/opengrep/github_pr.py @@ -218,16 +218,10 @@ def format_notifications(groups: Dict[str, List[Dict[str, Any]]], config=None) - # Build title - just scanner name (repo/branch context already visible in PR) title = display_name - # Add full scan link at top if available (using shared helper) - scan_link_section = helpers.format_scan_link_section(full_scan_url) - - # Wrap content with HTML comment markers for section updates - wrapped_content = f""" -## {title} - -{scan_link_section} -{content} -""" + # Wrap in standard PR comment section + wrapped_content = helpers.wrap_pr_comment_section( + subtype, title, content, full_scan_url + ) tables.append({ 'title': title, diff --git a/socket_basics/core/connector/socket_tier1/github_pr.py b/socket_basics/core/connector/socket_tier1/github_pr.py index 975c2a7..0e33741 100644 --- a/socket_basics/core/connector/socket_tier1/github_pr.py +++ b/socket_basics/core/connector/socket_tier1/github_pr.py @@ -281,16 +281,10 @@ def format_notifications(components_list: List[Dict[str, Any]], config=None) -> # Content already includes summary and details sections summary_content = content - # Add full scan link at top if available (using shared helper) - scan_link_section = helpers.format_scan_link_section(full_scan_url) - - # Wrap content with HTML comment markers for section updates - wrapped_content = f""" -## {title} - -{scan_link_section} -{summary_content} -""" + # Wrap in standard PR comment section + wrapped_content = helpers.wrap_pr_comment_section( + 'socket-tier1', title, summary_content, full_scan_url + ) return [{ 'title': title, diff --git a/socket_basics/core/connector/trivy/github_pr.py b/socket_basics/core/connector/trivy/github_pr.py index 000c14e..22ac274 100644 --- a/socket_basics/core/connector/trivy/github_pr.py +++ b/socket_basics/core/connector/trivy/github_pr.py @@ -313,16 +313,10 @@ def format_notifications(mapping: Dict[str, Any], item_name: str = "Unknown", sc {content}""" - # Add full scan link at top if available (using shared helper) - scan_link_section = helpers.format_scan_link_section(full_scan_url) - - # Wrap content with HTML comment markers for section updates - wrapped_content = f""" -## {title} - -{scan_link_section} -{summary_content} -""" + # Wrap in standard PR comment section + wrapped_content = helpers.wrap_pr_comment_section( + 'trivy-container', title, summary_content, full_scan_url + ) return [{ 'title': title, diff --git a/socket_basics/core/connector/trufflehog/github_pr.py b/socket_basics/core/connector/trufflehog/github_pr.py index 23aa7f9..58b2f19 100644 --- a/socket_basics/core/connector/trufflehog/github_pr.py +++ b/socket_basics/core/connector/trufflehog/github_pr.py @@ -94,16 +94,10 @@ def format_notifications(mapping: Dict[str, Any], config=None) -> List[Dict[str, {content}""" - # Add full scan link at top if available (using shared helper) - scan_link_section = helpers.format_scan_link_section(full_scan_url) - - # Wrap content with HTML comment markers for section updates - wrapped_content = f""" -## {title} - -{scan_link_section} -{summary_content} -""" + # Wrap in standard PR comment section + wrapped_content = helpers.wrap_pr_comment_section( + 'trufflehog-secrets', title, summary_content, full_scan_url + ) return [{ 'title': title, From 083f312fe7b8b54ec5c9501d76b9fb28b57c6230 Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 17:01:26 -0800 Subject: [PATCH 22/29] Wrap connector logic, add support for 32px logo --- .../core/notification/github_pr_helpers.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/socket_basics/core/notification/github_pr_helpers.py b/socket_basics/core/notification/github_pr_helpers.py index 27313b8..42c95ab 100644 --- a/socket_basics/core/notification/github_pr_helpers.py +++ b/socket_basics/core/notification/github_pr_helpers.py @@ -28,6 +28,59 @@ 'low': 'โšช' } +# 32px logo for PR comment headers (PNG file stored in assets/) +# TODO: Switch back to main branch URL once assets/socket-logo.png before merging. +# Using feature branch URL for now so the logo renders during PR testing. +# SOCKET_LOGO_URL = 'https://raw.githubusercontent.com/SocketDev/socket-basics/main/assets/socket-logo.png' +SOCKET_LOGO_URL = 'https://raw.githubusercontent.com/SocketDev/socket-basics/lelia/pr-comment-enhancements/assets/socket-logo.png' +SOCKET_LOGO_IMG = f'' + + +def format_title_with_logo(title: str) -> str: + """Format an H2 title with the Socket logo inline. + + Args: + title: The title text (e.g., "Socket SAST JavaScript") + + Returns: + Markdown H2 header with inline logo image + """ + return f"## {SOCKET_LOGO_IMG} {title}" + + +def wrap_pr_comment_section( + section_id: str, + title: str, + body: str, + full_scan_url: Optional[str] = None +) -> str: + """Wrap scanner output in the standard PR comment structure. + + Every scanner section gets the same wrapper: HTML comment markers for + idempotent updates, a branded H2 title with logo, an optional scan + report link, and the scanner-specific body content. + + Args: + section_id: Unique identifier for this section (e.g., "socket-tier1", + "trivy-container", "sast-javascript"). Used in HTML comment + markers so the notifier can find and update existing sections. + title: Display title (e.g., "Socket SAST JavaScript") + body: Scanner-specific markdown content (summary tables, findings, etc.) + full_scan_url: Optional URL to full scan report + + Returns: + Complete markdown section ready to post as a PR comment + """ + title_header = format_title_with_logo(title) + scan_link = format_scan_link_section(full_scan_url) + + return f""" +{title_header} + +{scan_link} +{body} +""" + # ============================================================================ # Configuration Helper From 4b4624edd9f6859af3c6162b933bd4f799400f90 Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 17:01:44 -0800 Subject: [PATCH 23/29] Add 32x32px Socket logo asset Signed-off-by: lelia --- assets/socket-logo.png | Bin 0 -> 1591 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/socket-logo.png diff --git a/assets/socket-logo.png b/assets/socket-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cbcb7d5e19c9aff2dfb589dc94e85812363a07e4 GIT binary patch literal 1591 zcmV-72FUq|P)?HeKfmmiwaJK%(w&;lo(pvQuV0% z%=-KBu||R9MG4iYfDHgD0A+CR6lpNNQW7>OsTu>Ox_GCjukqlf!Sx3|8mdw|)wp>) z4h2PO=E-P!&dF6Hb5HG&V`i%|GlzMm^_+Mf;LM7gyxh4&))+x5$Wa)JdQ+;yim0{e z_UNAP>l3T@Z&p!_uz*hEA%N+JqNYq9GtKl#PP&Z=RWmNk0lj&c4co2a5*^)!+GA8$ zDpZ58O^U#Yk&rsJcPp5L1+^M4foL$m!)rKbjJ+&!%OxkUJaKW59KgoeI;$)w9dlWv z1OeM;w_XCajR`x#IGrdJC|e#wd2I~SRzy&-Jc7!XN>Mt~@QvWqK8s%!e#2Ds^17$6 z@Z0%V^vzr>`FakP?w*C39n%q=AIy`ta`VI|2{dM}PTIJeI&J8oZBoWMBa zbmGz#F+h+fj7llK1tC21W)x4?lt7;o4`2dC6!rO5E}JeeBZ$!ANWqYj3Io+!E3x>~ zY1Zl}K9I0tSWN-mZ73wbZ>JmJmczBY{Co)AHcKXMiO}7WK<{;Z6SU4Xe<`(6v=$KH zC$uPSG6*F+dpT^v9Lhi@^)&M_NA9H2bubR4527aIi{hwqU?4w6OF}ofI+#{GHks4i z0_W@h?jE`}w&R~o9T+-2l=GUS5}o@8FxG4GZw;s7VozQRHjLx4pi=x4=Z$Rf5->uN ze>~#$VD>sikKi9Cn1dN)deSiCCPE8BmRIlhgHywN3les6-U{2HTRW|mtf~+X@Zwpr z(|8HQ`qD&n%kkwpUl@q6Kyar079_foxZB*He*-jI6#t0SEg_M1d%Xm@8j=d?JfC2h z2l>+jkr$)<6bndR`g=`%NIr7KB!@x4Fyd#D&NVc_v&9|)1U*`u=RW(YFb^jnOW6`$ z6bATE_M93-f9o(Web%FU(v8msy{n_7q>k+g=pg|929$YCf^M)_v=IMeD48F^*ndgf z|EUKm?Zk7sTxN?$&d9OU^@?!d8VjCMK%lDU>IfM)z^DvYtGL4$5rJ?`6c6@2MCze? z=`AQXoS6%6qb+j_P?}72;QhTAf$u9IK-ei$C~XGO-&w4Vmz53vAsW6A$I#h0>?B!_ z?lqF>a?x703aG!#bm|vA;64vfKtK;F`@gOiWb6ir@0K@@{x{U~;{(XFg#Nq{`3P&H zuB03GDoT(vzuMAV)~hZ4T9g3sVH{9>M|l-~C4m2J=ZB4Tj3U$PTKc@pk{ECGc0*D? zHp^h_C~-{N=531-(2#j|KNvMb*r6a=d8v6S+6q`tZBDao-j-~Y-*i&yd^lk2_@s%( zY-3?NE&&a#y1DM2DdcAWUFAAqTI4|#PY2az zf;vYadpk&bidV%Lq38@V*u1g1tfM$8Z=I9?fz*QbSKCV?p$|Fx4+==|mgFON6dExU z_8B2;+IXajZxJ!v1uY^@N+3IC`sLbgqcZUY^QkAID{Qw;A`@g2(gM^bfRB}&{pI>Y zGw!p0N`=Qv31@+xJ-v2p$<^8;2nOE(#ODCD@*H_ p009602yM)C00006Nkl Date: Fri, 6 Feb 2026 17:25:59 -0800 Subject: [PATCH 24/29] Overhaul git ignore with better structure and categories Signed-off-by: lelia --- .gitignore | 137 ++++++++++++++++++++++++----------------------------- 1 file changed, 63 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index 7534bfd..c56fa24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,67 +1,54 @@ - -.idea -venv -.venv -build -dist -*.build -*.dist -*.egg-info -test -*.env -run_container.sh -*.zip -bin -scripts/*.py -*.json -markdown_overview_temp.md -markdown_security_temp.md +# OS files .DS_Store -*.pyc -test.py -# Note: requirements.txt is no longer needed - using pyproject.toml + uv.lock instead -# Version files are auto-managed by .hooks/version-check.py -*.cpython-312.pyc -file_generator.py -.env -# Allow only specific markdown files (documentation), ignore all others -# Negations must come BEFORE the ignore rule -!README.md -!docs/*.md -!tests/README.md -*.md -test_results -local_tests/ -custom_rules/ +# IDEs and editors +.idea/ +.vscode/ +*.sublime-workspace +*.sublime-project +*.swp +*~ -# Common Python ignores +# Python __pycache__/ *.py[cod] *$py.class +.python-version + +# Virtual environments +venv/ +.venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# Build and distribution +build/ +dist/ +*.build +*.dist +*.egg-info +.eggs/ +*.egg + +# Testing and coverage .pytest_cache/ .mypy_cache/ .coverage .coverage.* htmlcov/ +coverage/ +coverage.xml +nosetests.xml +test-results/ +test_results/ + +# pip pip-wheel-metadata/ pip-log.txt pip-delete-this-directory.txt -# Virtual environments -env/ -ENV/ -env.bak/ -venv.bak/ - -# IDEs and editors -.vscode/ -.idea/ -*.sublime-workspace -*.sublime-project -*.swp -*~ - # Node node_modules/ npm-debug.log* @@ -69,26 +56,19 @@ yarn-debug.log* yarn-error.log* .pnp/ -# Build and distribution -.eggs/ -*.egg -dist/ -build/ - -# Coverage and test output -coverage/ -coverage.xml -nosetests.xml -test-results/ +# Jupyter +.ipynb_checkpoints/ # Logs and runtime files -logs/ - +*.log *.pid *.sock +logs/ -# OS files -.DS_Store +# Temporary files +*.tmp +*.temp +*.zip # Binary and compiled *.exe @@ -96,16 +76,25 @@ logs/ *.so *.dylib -# Jupyter -.ipynb_checkpoints/ - -# Local temporary files -*.tmp -*.temp -# Ignore output logs and generated src files -*.log +# Environment and secrets +*.env -.python-version +# Data files (generated) +*.json .socket.fact.json -custom_rules/ \ No newline at end of file +# Markdown: ignore all except documentation +*.md +!README.md +!docs/*.md +!tests/README.md + +# Project-specific (local scripts and test files) +test/ +test.py +run_container.sh +bin/ +scripts/*.py +file_generator.py +local_tests/ +custom_rules/ From 8e20172ad9544591138bdd7ceb5e5cd7ccb8f576 Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 17:27:06 -0800 Subject: [PATCH 25/29] Update README to reflect new GHA functionality, dedupe usage examples Signed-off-by: lelia --- README.md | 178 +++++++----------------------------------------------- 1 file changed, 22 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index 65bc09c..1a0e74e 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,28 @@ Socket Basics orchestrates multiple security scanners, normalizes their outputs The easiest way to use Socket Basics is through GitHub Actions. Add it to your workflow in minutes: ```yaml -name: Security Scan +# .github/workflows/socket.yml + +name: โšก๏ธ Security Scan + on: pull_request: types: [opened, synchronize, reopened] +permissions: + contents: read + jobs: security-scan: permissions: issues: write contents: read pull-requests: write - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 15 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: Run Socket Basics + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: โšก๏ธ Run Socket Basics uses: SocketDev/socket-basics@1.0.28 env: GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} @@ -74,139 +80,24 @@ Socket Basics can also run locally or in other CI/CD environments: ## ๐ŸŽจ Enhanced PR Comments -Socket Basics delivers **beautifully formatted, actionable PR comments** with smart defaults that work out of the box. - -### Universal Enhancements - -These features work across **all scanner types** for a consistent experience: -- โœ… Socket Tier 1 (Reachability Analysis) -- โœ… SAST (OpenGrep/Semgrep) -- โœ… Container Scanning (Trivy) -- โœ… Secret Detection (TruffleHog) - -### What You Get (Enabled by Default) +Socket Basics delivers **beautifully formatted, actionable PR comments** with smart defaults โ€” all enabled by default, zero configuration needed. - ๐Ÿ”— **Clickable File Links** โ€” Jump directly to the vulnerable code in GitHub -- ๐Ÿ“‹ **Collapsible Sections** โ€” Critical findings auto-expand, others collapse for easy scanning -- ๐ŸŽจ **Syntax Highlighting** โ€” Language-aware code blocks for better readability -- ๐Ÿท๏ธ **Explicit Rule Names** โ€” Clear identification of which security rule was triggered -- ๐Ÿš€ **Quick Access Links** โ€” Full scan report link prominently displayed at the top -- ๐Ÿท๏ธ **Auto-Labels** โ€” PRs automatically tagged with severity-based labels (e.g., `security: critical`) - -### Using the Defaults - -**Zero configuration needed!** Just use the standard GitHub Actions setup: - -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} -``` - -All PR comment enhancements are **enabled by default** with sensible settings: -- Critical findings: **Auto-expanded** โœ… -- High/Medium/Low findings: **Collapsed** (click to expand) -- File links: **Clickable** with line numbers -- Code blocks: **Syntax highlighted** based on file type -- Labels: **`security: critical`**, **`security: high`**, **`security: medium`** - -### Customizing PR Comments - -Need different behavior? Every feature can be customized: - -#### Example: Disable Specific Features - -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} - # Customize PR comment behavior - pr_comment_links_enabled: 'false' # Disable clickable links - pr_comment_collapse_enabled: 'false' # Show all findings expanded - pr_labels_enabled: 'false' # Don't add labels to PRs -``` - -#### Example: Custom Label Names - -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} - # Use custom label names - pr_label_critical: 'socket: critical' # Instead of 'security: critical' - pr_label_high: 'socket: high' # Instead of 'security: high' - pr_label_medium: 'socket: medium' # Instead of 'security: medium' -``` +- ๐Ÿ“‹ **Collapsible Sections** โ€” Critical findings auto-expand, others collapse +- ๐ŸŽจ **Syntax Highlighting** โ€” Language-aware code blocks +- ๐Ÿท๏ธ **Auto-Labels** โ€” PRs tagged with severity-based labels (e.g., `security: critical`) +- ๐Ÿ”ด **CVE Links & CVSS Scores** โ€” One-click access to NVD with risk context +- ๐Ÿš€ **Full Scan Link** โ€” Report link prominently displayed at the top -#### Example: Show All Findings Expanded +Every feature is customizable via GitHub Actions inputs, CLI flags, or environment variables. -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} - # Keep collapsible sections but expand everything - pr_comment_collapse_enabled: 'true' - pr_comment_collapse_non_critical: 'false' # Don't collapse non-critical -``` - -### All PR Comment Configuration Options - -| Option | Default | Description | -|--------|---------|-------------| -| `pr_comment_links_enabled` | `true` | Enable clickable file/line links in PR comments | -| `pr_comment_collapse_enabled` | `true` | Enable collapsible sections in PR comments | -| `pr_comment_collapse_non_critical` | `true` | Auto-collapse non-critical findings (critical stays expanded) | -| `pr_comment_code_fencing_enabled` | `true` | Enable language-aware syntax highlighting | -| `pr_comment_show_rule_names` | `true` | Show explicit rule names for each finding | -| `pr_labels_enabled` | `true` | Add severity-based labels to PRs | -| `pr_label_critical` | `"security: critical"` | Label name for critical severity findings | -| `pr_label_high` | `"security: high"` | Label name for high severity findings | -| `pr_label_medium` | `"security: medium"` | Label name for medium severity findings | - -### CLI Usage - -These options are also available via CLI: - -```bash -socket-basics \ - --pr-comment-links \ - --pr-comment-collapse \ - --pr-labels \ - --pr-label-critical "custom: critical" \ - --workspace /path/to/repo -``` - -### Environment Variables - -Or via environment variables: - -```bash -export INPUT_PR_COMMENT_LINKS_ENABLED=true -export INPUT_PR_COMMENT_COLLAPSE_ENABLED=true -export INPUT_PR_LABELS_ENABLED=true -export INPUT_PR_LABEL_CRITICAL="security: critical" -socket-basics --workspace /path/to/repo -``` - -๐Ÿ“– **[Complete PR Comment Features Guide โ†’](docs/pr-comment-features.md)** +๐Ÿ“– **[PR Comment Guide โ†’](docs/github-pr-comment-guide.md)** โ€” Full customization options, examples, and configuration reference ## ๐Ÿ“– Documentation ### Getting Started - [GitHub Actions Integration](docs/github-action.md) โ€” Complete guide with workflow examples -- [PR Comment Features Guide](docs/pr-comment-features.md) โ€” Detailed guide to PR comment customization +- [PR Comment Guide](docs/github-pr-comment-guide.md) โ€” Detailed guide to PR comment customization - [Pre-Commit Hook Setup](docs/pre-commit-hook.md) โ€” Two installation methods (Docker vs native) - [Local Docker Installation](docs/local-install-docker.md) โ€” Run with Docker, no tools to install - [Local Installation](docs/local-installation.md) โ€” Install Socket CLI, Trivy, and other tools natively @@ -237,34 +128,9 @@ Configure scanning policies, notification channels, and rule sets for your entir ![Socket Basics Section Config](docs/screenshots/socket_basics_section_config.png) -## ๐Ÿ’ป Usage Examples - -### GitHub Actions (Recommended) - -**Dashboard-Configured (Enterprise):** -```yaml -- uses: SocketDev/socket-basics@1.0.28 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} - # All configuration managed in Socket Dashboard -``` - -**CLI-Configured:** -```yaml -- uses: SocketDev/socket-basics@1.0.28 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - python_sast_enabled: 'true' - secret_scanning_enabled: 'true' - container_images: 'myapp:latest' -``` +## ๐Ÿ’ป Other Usage Methods -๐Ÿ“– **[View Complete GitHub Actions Documentation](docs/github-action.md)** +For GitHub Actions, see the [Quick Start](#-quick-start---github-actions) above or the **[Complete GitHub Actions Guide](docs/github-action.md)** for advanced workflows. ### Docker From 630faa858af34796e586cbc1bb389fb9d193dc35 Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 17:27:39 -0800 Subject: [PATCH 26/29] Add README to cover unit/integration testing with pytest Signed-off-by: lelia --- tests/README.md | 258 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 tests/README.md diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..bfb7519 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,258 @@ +# Unit Tests + +This directory contains **unit and integration tests** for Socket Basics using pytest. + +## Quick Start + +```bash +# Setup (first time only) +python3 -m venv venv +source venv/bin/activate +pip install -e ".[dev]" + +# Run all tests +pytest + +# Run with coverage +pytest --cov=socket_basics +``` + +## Test Structure + +``` +tests/ +โ”œโ”€โ”€ test_github_helpers.py # Helper functions for GitHub PR comments +โ””โ”€โ”€ ... # Other unit tests +``` + +## Writing Tests + +### Test File Naming +- Test files: `test_*.py` +- Test functions: `test_*` +- Test classes: `Test*` + +### Example Test + +```python +import pytest +from socket_basics.module import function_to_test + +def test_basic_functionality(): + """Test basic functionality with valid input.""" + result = function_to_test("input") + assert result == "expected_output" + +def test_edge_case(): + """Test edge case handling.""" + result = function_to_test("") + assert result == "" + +def test_error_handling(): + """Test error handling.""" + with pytest.raises(ValueError): + function_to_test(None) +``` + +### Test Classes + +Use test classes to group related tests: + +```python +class TestGitHubHelpers: + """Tests for GitHub helper functions.""" + + def test_url_building(self): + url = build_github_file_url("owner/repo", "abc123", "file.py", 10) + assert "github.com" in url + + def test_language_detection(self): + lang = detect_language_from_filename("app.js") + assert lang == "javascript" +``` + +## Running Tests + +### Run All Tests +```bash +pytest +``` + +### Run Specific Test File +```bash +pytest tests/test_github_helpers.py +``` + +### Run Specific Test Function +```bash +pytest tests/test_github_helpers.py::test_detect_language_from_filename +``` + +### Run Specific Test Class +```bash +pytest tests/test_github_helpers.py::TestDetectLanguageFromFilename +``` + +### Run with Verbose Output +```bash +pytest -v +``` + +### Run with Coverage Report +```bash +# Terminal coverage report +pytest --cov=socket_basics tests/ + +# HTML coverage report +pytest --cov=socket_basics --cov-report=html tests/ +open htmlcov/index.html +``` + +### Run with Markers (Future Enhancement) +```bash +# Run only fast tests +pytest -m "not slow" + +# Run only integration tests +pytest -m integration +``` + +## Test Categories + +### Unit Tests +**Purpose:** Test individual functions in isolation +**Speed:** Fast (milliseconds) +**Dependencies:** None (use mocks) +**Location:** `tests/test_*.py` + +**Example:** +```python +def test_detect_language_from_filename(): + assert detect_language_from_filename('app.js') == 'javascript' + assert detect_language_from_filename('main.py') == 'python' +``` + +### Integration Tests (Future) +**Purpose:** Test multiple components together +**Speed:** Moderate (seconds) +**Dependencies:** Real configs, but mocked external APIs +**Location:** `tests/integration/test_*.py` + +**Example:** +```python +def test_formatter_with_config(): + config = create_test_config() + result = format_notifications(components, config) + assert len(result) > 0 +``` + +## Mocking External Dependencies + +Use pytest fixtures for common test data: + +```python +@pytest.fixture +def mock_config(): + """Provide a mock configuration object.""" + return { + 'repo': 'owner/repo', + 'commit_hash': 'abc123', + 'pr_comment_links_enabled': True + } + +def test_with_config(mock_config): + result = format_with_config(mock_config) + assert result is not None +``` + +## Testing Best Practices + +### โœ… DO: +- Keep tests fast and isolated +- Test one thing per test function +- Use descriptive test names +- Test both success and error cases +- Use fixtures for common test data +- Run tests before committing + +### โŒ DON'T: +- Make external API calls (mock them) +- Depend on test execution order +- Use hardcoded paths (use fixtures) +- Test implementation details +- Skip error case testing + +## Continuous Integration + +Tests run automatically on: +- Every pull request +- Every commit to main branch +- Before releases + +**GitHub Actions Example:** +```yaml +- name: Run Tests + run: | + pip install -e ".[dev]" + pytest --cov=socket_basics tests/ +``` + +## Debugging Tests + +### Run Single Test with Debug Output +```bash +pytest tests/test_github_helpers.py::test_name -vv -s +``` + +### Drop into debugger on failure +```bash +pytest --pdb +``` + +### Print statements in tests +```python +def test_example(): + result = function() + print(f"Debug: result = {result}") # Use -s flag to see output + assert result == expected +``` + +## Coverage Goals + +- **Target:** 80%+ code coverage for new features +- **Critical paths:** 100% coverage (authentication, security logic) +- **Helper functions:** 90%+ coverage + +Check coverage: +```bash +pytest --cov=socket_basics --cov-report=term-missing +``` + +## Related Testing + +For **end-to-end testing** with real vulnerable applications, see: +- `../app_tests/` - Deliberately vulnerable apps for scanner validation +- `../app_tests/README.md` - E2E testing documentation (if it exists) + +## Test Data + +Test fixtures and sample data should be: +- **Small** - Minimal data needed for the test +- **Realistic** - Representative of actual usage +- **Self-contained** - No external dependencies + +**Example:** +```python +SAMPLE_TRACE = """owasp-goat - server.js 72:12-75:6 + -> express routes/auth.js 45:2""" + +def test_trace_parsing(): + result = parse_trace(SAMPLE_TRACE) + assert len(result) == 2 +``` + +## Questions? + +- See main [README.md](../README.md) for project overview +- See [docs/](../docs/) for detailed documentation +- Check existing tests for examples From 241e14fe1e4214cee65ac26c5f6488fe5f1f188f Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 17:29:26 -0800 Subject: [PATCH 27/29] Clean up and deduplicate GHA docs Signed-off-by: lelia --- docs/github-action.md | 178 +++++------------------------------------- 1 file changed, 18 insertions(+), 160 deletions(-) diff --git a/docs/github-action.md b/docs/github-action.md index e0b639c..6c027db 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -28,31 +28,29 @@ on: pull_request: types: [opened, synchronize, reopened] +permissions: + contents: read + jobs: security-scan: permissions: issues: write contents: read pull-requests: write - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 15 steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Socket Basics uses: SocketDev/socket-basics@1.0.28 env: GITHUB_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} with: github_token: ${{ secrets.GITHUB_TOKEN }} - python_sast_enabled: 'true' - secret_scanning_enabled: 'true' + socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} ``` -This will: -- โœ… Run Python SAST on all `.py` files -- โœ… Scan for leaked secrets -- โœ… Post results as a PR comment -- โœ… Post results as a PR comment +With just your `SOCKET_SECURITY_API_KEY`, all scanning configurations are managed through the [Socket Dashboard](https://socket.dev/dashboard) โ€” no workflow changes needed. ## Basic Configuration @@ -139,149 +137,9 @@ Include these in your workflow's `jobs..permissions` section. ## PR Comment Customization -Socket Basics automatically posts enhanced PR comments with **smart defaults that work out of the box**. All features are enabled by default for the best developer experience. - -### Default Behavior (Zero Config) - -With the minimal configuration, you automatically get: - -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_tier_1_enabled: 'true' - # That's it! PR comments are automatically enhanced -``` - -**What you get by default:** -- โœ… **Clickable file links** โ€” Jump directly to vulnerable code in GitHub -- โœ… **Collapsible sections** โ€” Critical findings expanded, others collapsed -- โœ… **Syntax highlighting** โ€” Language-aware code blocks -- โœ… **Rule names** โ€” Clear identification of security rules -- โœ… **Quick access link** โ€” Full scan report at the top -- โœ… **Auto-labels** โ€” PRs tagged with `security: critical`, `security: high`, or `security: medium` - -### Customization Examples - -#### Disable Specific Features - -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_tier_1_enabled: 'true' - # Customize specific features - pr_comment_links_enabled: 'false' # Disable clickable links - pr_labels_enabled: 'false' # Don't add labels -``` - -#### Custom Label Names - -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_tier_1_enabled: 'true' - # Use organization-specific label taxonomy - pr_label_critical: 'socket: critical' - pr_label_high: 'socket: high' - pr_label_medium: 'socket: medium' -``` - -#### Show All Findings Expanded - -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_tier_1_enabled: 'true' - # Keep collapsible UI but expand everything - pr_comment_collapse_enabled: 'true' - pr_comment_collapse_non_critical: 'false' -``` - -#### Minimal/Plaintext Mode +Socket Basics automatically posts enhanced PR comments with **smart defaults that work out of the box** โ€” clickable file links, collapsible sections, syntax highlighting, CVE links, CVSS scores, and auto-labels are all enabled by default. -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_tier_1_enabled: 'true' - # Disable all enhancements for simple text output - pr_comment_links_enabled: 'false' - pr_comment_collapse_enabled: 'false' - pr_comment_code_fencing_enabled: 'false' - pr_comment_show_rule_names: 'false' - pr_labels_enabled: 'false' -``` - -### Complete PR Comment Options - -| Option | Default | Description | -|--------|---------|-------------| -| `pr_comment_links_enabled` | `true` | Enable clickable file/line links | -| `pr_comment_collapse_enabled` | `true` | Enable collapsible sections | -| `pr_comment_collapse_non_critical` | `true` | Auto-collapse non-critical (critical stays expanded) | -| `pr_comment_code_fencing_enabled` | `true` | Enable language-aware syntax highlighting | -| `pr_comment_show_rule_names` | `true` | Show explicit rule names | -| `pr_labels_enabled` | `true` | Add severity-based labels to PRs | -| `pr_label_critical` | `"security: critical"` | Label name for critical findings | -| `pr_label_high` | `"security: high"` | Label name for high findings | -| `pr_label_medium` | `"security: medium"` | Label name for medium findings | - -### Real-World Examples - -**Example 1: Enterprise with Custom Taxonomy** -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_security_api_key: ${{ secrets.SOCKET_SECURITY_API_KEY }} - # Match your organization's label taxonomy - pr_label_critical: 'vulnerability: critical' - pr_label_high: 'vulnerability: high' - pr_label_medium: 'vulnerability: medium' -``` - -**Example 2: OSS Project (Minimize Noise)** -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_tier_1_enabled: 'true' - # Keep it clean - collapse everything by default - pr_comment_collapse_non_critical: 'true' - # Use simple labels - pr_label_critical: 'security' - pr_label_high: 'security' - pr_label_medium: 'security' -``` - -**Example 3: Security Team (All Details Visible)** -```yaml -- uses: SocketDev/socket-basics@1.0.26 - env: - GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - socket_tier_1_enabled: 'true' - # Show everything expanded for thorough review - pr_comment_collapse_non_critical: 'false' -``` +๐Ÿ“– **[PR Comment Guide โ†’](github-pr-comment-guide.md)** โ€” Complete customization options, configuration examples, and reference table ## Enterprise Features @@ -415,7 +273,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Socket Basics uses: SocketDev/socket-basics@1.0.28 @@ -461,7 +319,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Full Security Scan uses: SocketDev/socket-basics@1.0.28 @@ -512,7 +370,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Build Docker Image run: docker build -t myapp:1.0.28:${{ github.sha }} . @@ -550,7 +408,7 @@ jobs: outputs: dockerfiles: ${{ steps.discover.outputs.dockerfiles }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Discover Dockerfiles id: discover @@ -578,7 +436,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Socket Basics uses: SocketDev/socket-basics@1.0.28 @@ -630,7 +488,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run Socket Basics uses: SocketDev/socket-basics@1.0.28 @@ -722,10 +580,10 @@ env: **Problem:** Scanner reports no files found. -**Solution:** Ensure `actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683` runs before Socket Basics: +**Solution:** Ensure `actions/checkout` runs before Socket Basics: ```yaml steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - Must be first + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - Must be first - uses: SocketDev/socket-basics@1.0.28 ``` From e2870cf126e26dfa8233c2896601b4d557dc9577 Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 17:29:59 -0800 Subject: [PATCH 28/29] Update CI/CD install refs to defer to GHA docs Signed-off-by: lelia --- docs/local-install-docker.md | 4 ++++ docs/local-installation.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/local-install-docker.md b/docs/local-install-docker.md index 02b3c69..b2ffb99 100644 --- a/docs/local-install-docker.md +++ b/docs/local-install-docker.md @@ -335,6 +335,10 @@ done ### CI/CD Integration +> **Using GitHub Actions?** Socket Basics has first-class GitHub Actions support with automatic PR comments, labels, and more โ€” no Docker setup needed. See the [Quick Start](../README.md#-quick-start---github-actions) or the [GitHub Actions Guide](github-action.md). + +For other CI/CD platforms, use the Docker image directly: + **Example: Jenkins** ```groovy diff --git a/docs/local-installation.md b/docs/local-installation.md index 7cd3763..d36ff0a 100644 --- a/docs/local-installation.md +++ b/docs/local-installation.md @@ -537,6 +537,10 @@ socket-basics \ --verbose ``` +### CI/CD Integration + +> **Using GitHub Actions?** Socket Basics has first-class GitHub Actions support with automatic PR comments, labels, and more โ€” no local installation needed. See the [Quick Start](../README.md#-quick-start---github-actions) or the [GitHub Actions Guide](github-action.md). + ### Continuous Scanning Watch for file changes and re-scan: From d65ab2a9adbf08a9eeba05b1a36b65fc69869296 Mon Sep 17 00:00:00 2001 From: lelia Date: Fri, 6 Feb 2026 17:32:07 -0800 Subject: [PATCH 29/29] Add docs to cover new GitHub PR comment functionality and config Signed-off-by: lelia --- docs/github-pr-comment-guide.md | 495 ++++++++++++++++++++++++++++++++ 1 file changed, 495 insertions(+) create mode 100644 docs/github-pr-comment-guide.md diff --git a/docs/github-pr-comment-guide.md b/docs/github-pr-comment-guide.md new file mode 100644 index 0000000..6a9f7c3 --- /dev/null +++ b/docs/github-pr-comment-guide.md @@ -0,0 +1,495 @@ +# GitHub PR Comment Guide + +Socket Basics delivers **beautifully formatted, actionable GitHub PR comments** that help developers quickly understand and address security findings. + +## ๐ŸŒŸ Universal Features + +These enhancements work across **all scanner types**: +- โœ… **Socket Tier 1** (Reachability Analysis) +- โœ… **SAST** (OpenGrep/Semgrep) +- โœ… **Container Scanning** (Trivy) +- โœ… **Secret Detection** (TruffleHog) +- โœ… **Future OSS Tools** (via centralized architecture) + +All scanners share the same UX enhancements for a consistent, professional experience. + +## ๐ŸŽฏ Quick Start + +**Want the enhanced experience?** You already have it! All features are **enabled by default** with the standard workflow setup โ€” see the [Quick Start in README](../README.md#-quick-start---github-actions). + +## โœจ Features + +### 1. Clickable File Links (`pr_comment_links_enabled`) + +**Default:** `true` + +Jump directly to vulnerable code in GitHub with one click. + +**Before:** +``` +owasp-goat - server.js 72:12-75:6 + -> express routes/auth.js 45:2 +``` + +**After:** +``` +owasp-goat - [server.js 72:12-75:6](https://github.com/owner/repo/blob/abc123/server.js#L72-L75) + -> express [routes/auth.js 45:2](https://github.com/owner/repo/blob/abc123/routes/auth.js#L45) +``` + +**Disable:** +```yaml +pr_comment_links_enabled: 'false' +``` + +--- + +### 2. Collapsible Sections (`pr_comment_collapse_enabled`) + +**Default:** `true` (critical auto-expands, others collapse) + +Organize findings with expandable sections for easy scanning. + +**Before:** +```markdown +#### pkg:npm/lodash@4.17.20 + +๐Ÿ”ด CVE-2021-23337: CRITICAL +... + +๐ŸŸ  CVE-2021-23338: HIGH +... +``` + +**After:** +```markdown +
+pkg:npm/lodash@4.17.20 (๐Ÿ”ด Critical: 1 | ๐ŸŸ  High: 1) + +๐Ÿ”ด CVE-2021-23337: CRITICAL +... + +๐ŸŸ  CVE-2021-23338: HIGH +... + +
+``` + +**Options:** +```yaml +# Disable collapsible sections entirely +pr_comment_collapse_enabled: 'false' + +# Keep collapsible but expand everything +pr_comment_collapse_enabled: 'true' +pr_comment_collapse_non_critical: 'false' +``` + +--- + +### 3. Syntax Highlighting (`pr_comment_code_fencing_enabled`) + +**Default:** `true` + +Language-aware code blocks based on file extension. + +**Before:** +``` +owasp-goat - server.js 72:12-75:6 + -> express routes/auth.js 45:2 +``` + +**After:** +````markdown +```javascript +owasp-goat - [server.js 72:12-75:6](https://github.com/owner/repo/blob/abc123/server.js#L72-L75) + -> express [routes/auth.js 45:2](https://github.com/owner/repo/blob/abc123/routes/auth.js#L45) +``` +```` + +**Supported languages:** +- JavaScript/TypeScript (`.js`, `.jsx`, `.ts`, `.tsx`) +- Python (`.py`) +- Go (`.go`) +- Java (`.java`) +- Ruby (`.rb`) +- PHP (`.php`) +- And 20+ more... + +**Disable:** +```yaml +pr_comment_code_fencing_enabled: 'false' +``` + +--- + +### 4. Explicit Rule Names (`pr_comment_show_rule_names`) + +**Default:** `true` + +Clearly identify which security rule was triggered. + +**Before:** +``` +๐Ÿ”ด CVE-2021-23337: CRITICAL +``` + +**After:** +``` +๐Ÿ”ด CVE-2021-23337: CRITICAL +**Rule**: `CVE-2021-23337` +``` + +**Disable:** +```yaml +pr_comment_show_rule_names: 'false' +``` + +--- + +### 5. CVE Links & CVSS Scores + +**Default:** Always enabled (automatic for CVE/vulnerability findings) + +Make vulnerability IDs clickable and display CVSS scores for better risk assessment. + +**Before:** +```markdown +๐Ÿ”ด CVE-2021-23337: CRITICAL +``` + +**After:** +```markdown +๐Ÿ”ด **[CVE-2021-23337](https://nvd.nist.gov/vuln/detail/CVE-2021-23337)** โ€ข CRITICAL (CVSS 9.8) +``` + +**How it works:** +- CVE IDs automatically become clickable links to the National Vulnerability Database (NVD) +- CVSS scores are displayed when available from the scanner +- Works for Socket Tier 1 and Trivy container/CVE scanning +- Missing CVSS scores are gracefully omitted (no breaking changes) + +**Example with different formats:** +```markdown +# With CVSS score +๐Ÿ”ด **[CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228)** โ€ข CRITICAL (CVSS 10.0) + +# Without CVSS score (not available) +๐ŸŸ  **[CVE-2021-23338](https://nvd.nist.gov/vuln/detail/CVE-2021-23338)** โ€ข HIGH + +# Non-CVE vulnerabilities (GHSA, etc.) +๐Ÿ”ด **GHSA-abcd-1234-efgh** โ€ข CRITICAL +``` + +**Benefits:** +- One-click access to detailed vulnerability information +- CVSS scores provide standardized risk context +- Helps developers prioritize remediation efforts +- Links to official NVD entries with full CVE details + +--- + +### 6. Full Scan Link at Top + +**Default:** Always enabled (when URL available) + +Quick access to the complete scan report. + +**Before:** +```markdown +# Socket Security Tier 1 Results + +### Summary +๐Ÿ”ด Critical: 3 | ๐ŸŸ  High: 14 | ๐ŸŸก Medium: 0 | โšช Low: 0 + +### Details +... + +--- +๐Ÿ”— [View Full Socket Scan](https://socket.dev/scan/123) +``` + +**After:** +```markdown +# Socket Security Tier 1 Results + +๐Ÿ”— **[View Full Socket Scan Report](https://socket.dev/scan/123)** + +--- + +### Summary +๐Ÿ”ด Critical: 3 | ๐ŸŸ  High: 14 | ๐ŸŸก Medium: 0 | โšช Low: 0 + +### Details +... +``` + +--- + +### 7. Auto-Labels with Colors (`pr_labels_enabled`) + +**Default:** `true` + +Automatically tag PRs with severity-based labels **and matching colors**. + +**Labels added (with automatic color assignment):** +- `security: critical` ๐Ÿ”ด - Red (`#D73A4A`) +- `security: high` ๐ŸŸ  - Orange (`#D93F0B`) +- `security: medium` ๐ŸŸก - Yellow (`#FBCA04`) + +**Smart color detection:** +Labels are automatically created with colors matching the severity emojis. If you customize label names, the system intelligently detects severity keywords and applies appropriate colors: + +```yaml +pr_label_critical: 'vulnerability: critical' # Gets red color automatically +pr_label_high: 'security-high' # Gets orange color automatically +``` + +**How it works:** +- First scan checks for critical โ†’ high โ†’ medium (highest severity wins) +- Labels are created automatically if they don't exist +- Existing labels are not modified (preserves your customizations) + +**Customize:** +```yaml +pr_labels_enabled: 'true' +pr_label_critical: 'vulnerability: critical' +pr_label_high: 'vulnerability: high' +pr_label_medium: 'vulnerability: medium' +``` + +**Disable:** +```yaml +pr_labels_enabled: 'false' +``` + +--- + +### 8. Logo Branding + +**Default:** Always enabled + +Every PR comment section includes the Socket shield logo inline with the title header for consistent branding. + +```markdown +## Socket Security Tier 1 +``` + +The logo is a 32px PNG rendered at 24x24 for retina-crisp display, with a transparent background that works in both GitHub light and dark modes. + +--- + +## ๐Ÿ“‹ Configuration Reference + +### All Options + +| Option | Default | Type | Description | +|--------|---------|------|-------------| +| `pr_comment_links_enabled` | `true` | boolean | Enable clickable file/line links | +| `pr_comment_collapse_enabled` | `true` | boolean | Enable collapsible sections | +| `pr_comment_collapse_non_critical` | `true` | boolean | Auto-collapse non-critical findings | +| `pr_comment_code_fencing_enabled` | `true` | boolean | Enable syntax highlighting | +| `pr_comment_show_rule_names` | `true` | boolean | Show explicit rule names | +| `pr_labels_enabled` | `true` | boolean | Add severity-based labels to PRs | +| `pr_label_critical` | `"security: critical"` | string | Label name for critical findings | +| `pr_label_high` | `"security: high"` | string | Label name for high findings | +| `pr_label_medium` | `"security: medium"` | string | Label name for medium findings | + +### Configuration Methods + +**1. GitHub Actions (Recommended)** + +Add these parameters to the `with:` block in your workflow (see [Quick Start](../README.md#-quick-start---github-actions)): +```yaml +pr_comment_links_enabled: 'true' +pr_label_critical: 'security: critical' +``` + +**2. CLI Arguments** +```bash +socket-basics \ + --pr-comment-links \ + --pr-comment-collapse \ + --pr-label-critical "security: critical" +``` + +**3. Environment Variables** +```bash +export INPUT_PR_COMMENT_LINKS_ENABLED=true +export INPUT_PR_LABEL_CRITICAL="security: critical" +``` + +--- + +## ๐ŸŽจ Common Configurations + +All examples below show only the `with:` parameters to add to your workflow. See the [Quick Start](../README.md#-quick-start---github-actions) for the full workflow setup. + +### Default (Recommended) + +Everything enabled with sensible defaults โ€” no extra parameters needed. + +### Minimal (Plaintext) + +Simple text output without enhancements: +```yaml +pr_comment_links_enabled: 'false' +pr_comment_collapse_enabled: 'false' +pr_comment_code_fencing_enabled: 'false' +pr_comment_show_rule_names: 'false' +pr_labels_enabled: 'false' +``` + +### Enterprise (Custom Labels) + +Match your organization's label taxonomy: +```yaml +pr_label_critical: 'vulnerability: critical' +pr_label_high: 'vulnerability: high' +pr_label_medium: 'vulnerability: medium' +``` + +### Security Team (All Expanded) + +Show all details expanded for thorough review: +```yaml +pr_comment_collapse_non_critical: 'false' +``` + +### OSS Project (Minimize Noise) + +Keep comments clean and collapsed: +```yaml +pr_comment_collapse_non_critical: 'true' +pr_label_critical: 'security' +pr_label_high: 'security' +pr_label_medium: 'security' +``` + +--- + +## ๐Ÿš€ Migration Guide + +### Already using Socket Basics? + +**Good news!** All new features are **opt-out** with sensible defaults. + +**Your existing workflows will automatically benefit from:** +- โœ… Clickable file links +- โœ… Collapsible sections +- โœ… Syntax highlighting +- โœ… Rule names +- โœ… Auto-labels + +**No changes required** unless you want to customize behavior. + +### Disable specific features + +If you prefer the old style, simply disable individual features: + +```yaml +# Keep everything except labels +pr_labels_enabled: 'false' + +# Or disable everything +pr_comment_links_enabled: 'false' +pr_comment_collapse_enabled: 'false' +pr_comment_code_fencing_enabled: 'false' +pr_comment_show_rule_names: 'false' +pr_labels_enabled: 'false' +``` + +--- + +## ๐Ÿ’ก Tips & Best Practices + +### For Open Source Projects +- Use simple label taxonomy (e.g., all `security`) +- Keep non-critical findings collapsed +- Enable all visual enhancements for contributor UX + +### For Enterprise Teams +- Match your organization's label taxonomy +- Consider expanding all findings for security review +- Customize labels to integrate with existing workflows + +### For Security Teams +- Expand all findings by default +- Enable all enhancements for maximum detail +- Use specific label names for automation + +--- + +## ๐Ÿ—๏ธ Architecture & Extensibility + +### Centralized PR Comment Logic + +All PR comment enhancements are powered by a **shared helper module** at: +``` +socket_basics/core/notification/github_pr_helpers.py +``` + +This centralized approach provides: +- **Consistent UX** across all scanner types +- **Zero code duplication** - one implementation for all formatters +- **Easy integration** for new OSS security tools + +### Adding Your Own Security Tool + +Socket Basics is designed to support **any security scanner**. To add enhancements to a new tool: + +```python +from socket_basics.core.notification import github_pr_helpers as helpers + +def format_notifications(data, config=None): + # 1. Get feature flags (handles all config sources automatically) + flags = helpers.get_feature_flags(config) + + # 2. Use shared utilities to build your content + file_link = helpers.format_file_location_link( + filepath, line_start=line_num, repository=flags['repository'], + commit_hash=flags['commit_hash'], enable_links=flags['enable_links'] + ) + + code_block = helpers.format_code_block( + code_snippet, filepath=filepath, + enable_fencing=flags['enable_code_fencing'] + ) + + collapsible = helpers.create_collapsible_section( + title, content, severity_counts, + auto_expand=(critical and not flags['collapse_non_critical']) + ) + + # 3. Wrap in the standard PR comment section (logo, scan link, markers) + wrapped = helpers.wrap_pr_comment_section( + 'my-scanner', title, body_content, flags['full_scan_url'] + ) + + return [{'title': title, 'content': wrapped}] +``` + +**That's it!** Your tool instantly gets: +- โœ… Clickable file/line links +- โœ… Collapsible sections +- โœ… Syntax highlighting +- โœ… Logo branding and full scan report link +- โœ… Idempotent comment updates (via HTML markers) +- โœ… Configuration management + +### Shared Constants + +Use shared severity constants for consistency: +```python +severity_order = helpers.SEVERITY_ORDER # {'critical': 0, 'high': 1, ...} +severity_emoji = helpers.SEVERITY_EMOJI # {'critical': '๐Ÿ”ด', 'high': '๐ŸŸ ', ...} +``` + +--- + +## ๐Ÿ“š Related Documentation + +- [Main README](../README.md) +- [GitHub Actions Integration](github-action.md) +- [Configuration Examples](../socket_config_example.json) +- [Shared Helper Module](../socket_basics/core/notification/github_pr_helpers.py)