From f68ab291b5b25c00c73e979d8137012a592e144a Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:47:27 -0400 Subject: [PATCH 01/17] fix: support local SAST ignore overrides by rule id, path Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- action.yml | 10 +- docs/github-action.md | 18 +++ docs/parameters.md | 21 ++++ socket_basics/connectors.yaml | 6 + socket_basics/core/config.py | 108 +++++++++++++++++ .../core/connector/opengrep/__init__.py | 3 + tests/test_sast_ignore_overrides.py | 110 ++++++++++++++++++ 7 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 tests/test_sast_ignore_overrides.py diff --git a/action.yml b/action.yml index dcbbfc4..6cec1e2 100644 --- a/action.yml +++ b/action.yml @@ -4,7 +4,8 @@ author: "Socket" runs: using: "docker" - image: "docker://ghcr.io/socketdev/socket-basics:2.0.2" + # TODO: revert to the published GHCR image before merging/releasing. + image: "Dockerfile" env: # Core GitHub variables (these are automatically available, but we explicitly pass GITHUB_TOKEN) GITHUB_TOKEN: ${{ inputs.github_token }} @@ -40,6 +41,7 @@ runs: INPUT_JAVASCRIPT_DISABLED_RULES: ${{ inputs.javascript_disabled_rules }} INPUT_JAVASCRIPT_ENABLED_RULES: ${{ inputs.javascript_enabled_rules }} INPUT_JAVASCRIPT_SAST_ENABLED: ${{ inputs.javascript_sast_enabled }} + INPUT_SAST_IGNORE_OVERRIDES: ${{ inputs.sast_ignore_overrides }} INPUT_JAVA_DISABLED_RULES: ${{ inputs.java_disabled_rules }} INPUT_JAVA_ENABLED_RULES: ${{ inputs.java_enabled_rules }} INPUT_JAVA_SAST_ENABLED: ${{ inputs.java_sast_enabled }} @@ -246,6 +248,10 @@ inputs: description: "Enable JavaScript/TypeScript SAST scanning" required: false default: "false" + sast_ignore_overrides: + description: "Comma-separated list of SAST ignore overrides in rule_id or rule_id:path format" + required: false + default: "" jira_api_token: description: "Jira Api Token" required: false @@ -453,4 +459,4 @@ inputs: branding: icon: "shield" - color: "blue" + color: "purple" diff --git a/docs/github-action.md b/docs/github-action.md index 3297c12..64f5dcf 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -638,8 +638,25 @@ jobs: # JavaScript with custom rules javascript_sast_enabled: 'true' javascript_enabled_rules: 'eval-usage,prototype-pollution' + + # Ignore a specific SAST rule globally or for one exact file + sast_ignore_overrides: 'js-sql-injection:index.js' ``` +`sast_ignore_overrides` supports: +- `rule_id` to ignore a SAST rule everywhere in the repo +- `rule_id:path` to ignore a SAST rule for one exact repo-relative file + +Examples: +- `js-sql-injection` +- `js-sql-injection:index.js` +- `js-sql-injection:src/unsafe/demo.js` + +Notes: +- Paths must be exact repo-relative paths using `/` separators after normalization. +- Windows-style input such as `src\\unsafe\\demo.js` is accepted and normalized automatically. +- Globs and directory-prefix matching are not supported in this first version. + ## Configuration Reference ### All Available Inputs @@ -667,6 +684,7 @@ See [`action.yml`](../action.yml) for the complete list of inputs. **Rule Configuration (per language):** - `_enabled_rules` — Comma-separated rules to enable - `_disabled_rules` — Comma-separated rules to disable +- `sast_ignore_overrides` — Comma-separated `rule_id` or `rule_id:path` SAST ignore overrides **Security Scanning:** - `secret_scanning_enabled` — Enable secret scanning diff --git a/docs/parameters.md b/docs/parameters.md index c30e785..c422a43 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -241,6 +241,25 @@ socket-basics --go --go-enabled-rules "error-handling,sql-injection" - `--rust-enabled-rules` / `--rust-disabled-rules` - `--elixir-enabled-rules` / `--elixir-disabled-rules` +### `--sast-ignore-overrides SAST_IGNORE_OVERRIDES` +Comma-separated list of SAST ignore overrides in `rule_id` or `rule_id:path` format. + +**Environment Variable:** `INPUT_SAST_IGNORE_OVERRIDES` + +**Examples:** +```bash +# Ignore a rule everywhere in the repo +socket-basics --javascript --sast-ignore-overrides "js-sql-injection" + +# Ignore a rule only for one exact repo-relative file +socket-basics --javascript --sast-ignore-overrides "js-sql-injection:index.js" +``` + +Notes: +- Paths must be exact repo-relative paths. +- Paths are normalized to forward-slash form, so Windows-style input such as `src\\unsafe\\demo.js` is accepted. +- Globs and directory-prefix matching are not supported in this first version. + ### `--opengrep-notify OPENGREP_NOTIFY` Notification method for OpenGrep SAST results (e.g., console, slack). @@ -520,6 +539,7 @@ All notification integrations support environment variables as alternatives to C | Variable | Description | |----------|-------------| | `INPUT_OPENGREP_RULES_DIR` | Custom directory containing SAST rules | +| `INPUT_SAST_IGNORE_OVERRIDES` | Comma-separated `rule_id` or `rule_id:path` SAST ignore overrides | ## Configuration File @@ -537,6 +557,7 @@ You can provide configuration via a JSON file using `--config`: "python_sast_enabled": true, "javascript_sast_enabled": true, "go_sast_enabled": true, + "sast_ignore_overrides": "js-sql-injection:index.js", "secrets_enabled": true, "trufflehog_exclude_dir": "node_modules,vendor,dist,.git", diff --git a/socket_basics/connectors.yaml b/socket_basics/connectors.yaml index 815a85e..7df8b03 100644 --- a/socket_basics/connectors.yaml +++ b/socket_basics/connectors.yaml @@ -191,6 +191,12 @@ connectors: env_variable: INPUT_JAVASCRIPT_DISABLED_RULES type: str default: "" + - name: sast_ignore_overrides + option: --sast-ignore-overrides + description: "Comma-separated list of SAST ignore overrides in rule_id or rule_id:path format" + env_variable: INPUT_SAST_IGNORE_OVERRIDES + type: str + default: "" # Go rule configuration - name: go_enabled_rules diff --git a/socket_basics/core/config.py b/socket_basics/core/config.py index 55512cf..6f20125 100644 --- a/socket_basics/core/config.py +++ b/socket_basics/core/config.py @@ -15,6 +15,107 @@ logger = logging.getLogger(__name__) +def normalize_repo_relative_path(path_value: str | None) -> str | None: + """Normalize a repo-relative path to the POSIX form emitted by SAST alerts.""" + if path_value is None: + return None + + path_str = str(path_value).strip() + if not path_str: + return None + + # Accept common local input styles but keep the final format strict. + path_str = path_str.replace('\\', '/') + while path_str.startswith('./'): + path_str = path_str[2:] + path_str = path_str.lstrip('/') + + normalized_parts: List[str] = [] + for part in path_str.split('/'): + if not part or part == '.': + continue + if part == '..': + return None + normalized_parts.append(part) + + normalized = '/'.join(normalized_parts) + return normalized or None + + +def parse_sast_ignore_overrides(raw_value: str | None) -> List[Dict[str, str | None]]: + """Parse `rule_id` and `rule_id:path` ignore override entries.""" + overrides: List[Dict[str, str | None]] = [] + seen: set[tuple[str, str | None]] = set() + + if not raw_value: + return overrides + + for raw_entry in str(raw_value).split(','): + entry = raw_entry.strip() + if not entry: + continue + + rule_id = entry + path = None + + if ':' in entry: + rule_id, path_part = entry.split(':', 1) + rule_id = rule_id.strip() + path_part = path_part.strip() + + if not rule_id or not path_part: + logger.warning("Ignoring malformed SAST ignore override: %r", entry) + continue + + if any(ch in path_part for ch in ('*', '?', '[')): + logger.warning( + "Ignoring unsupported SAST ignore override with glob syntax: %r", + entry, + ) + continue + + path = normalize_repo_relative_path(path_part) + if not path: + logger.warning("Ignoring invalid repo-relative path in SAST override: %r", entry) + continue + else: + rule_id = rule_id.strip() + if not rule_id: + logger.warning("Ignoring malformed SAST ignore override: %r", entry) + continue + + key = (rule_id, path) + if key in seen: + continue + seen.add(key) + overrides.append({'rule_id': rule_id, 'path': path}) + + return overrides + + +def alert_matches_sast_ignore_override( + alert: Dict[str, Any], + override: Dict[str, str | None], +) -> bool: + """Return True when an alert matches a parsed SAST ignore override.""" + props = alert.get('props', {}) or {} + rule_id = props.get('ruleId') or alert.get('title') or alert.get('ruleId') + if not rule_id or rule_id != override.get('rule_id'): + return False + + override_path = override.get('path') + if not override_path: + return True + + alert_path = ( + props.get('filePath') + or (alert.get('location') or {}).get('path') + or '' + ) + normalized_alert_path = normalize_repo_relative_path(alert_path) + return normalized_alert_path == override_path + + class Config: """Configuration object that provides unified access to all settings""" @@ -112,6 +213,13 @@ def get_action_for_severity(self, severity: str) -> str: else: # Default action for unknown severities return 'monitor' + + def get_sast_ignore_overrides(self) -> List[Dict[str, str | None]]: + """Return parsed SAST ignore overrides from config.""" + if not hasattr(self, '_sast_ignore_overrides_cache'): + raw_value = self.get('sast_ignore_overrides', '') + self._sast_ignore_overrides_cache = parse_sast_ignore_overrides(raw_value) + return self._sast_ignore_overrides_cache @property def repo(self) -> str: diff --git a/socket_basics/core/connector/opengrep/__init__.py b/socket_basics/core/connector/opengrep/__init__.py index 0cf4135..47dc9d5 100644 --- a/socket_basics/core/connector/opengrep/__init__.py +++ b/socket_basics/core/connector/opengrep/__init__.py @@ -720,6 +720,9 @@ def generate_notifications(self, components: List[Dict[str, Any]]) -> Dict[str, groups: Dict[str, List[Dict[str, Any]]] = {} for c in comps_map.values(): for a in c.get('alerts', []): + alert_action = (a.get('action') or '').strip().lower() + if alert_action == 'ignore': + continue # Filter by severity - only include alerts that match allowed severities alert_severity = (a.get('severity') or '').strip().lower() if alert_severity and hasattr(self, 'allowed_severities') and alert_severity not in self.allowed_severities: diff --git a/tests/test_sast_ignore_overrides.py b/tests/test_sast_ignore_overrides.py new file mode 100644 index 0000000..0bb5678 --- /dev/null +++ b/tests/test_sast_ignore_overrides.py @@ -0,0 +1,110 @@ +from socket_basics.core.config import ( + Config, + parse_sast_ignore_overrides, +) +from socket_basics.core.connector.normalizer import _normalize_alert +from socket_basics.core.connector.opengrep import OpenGrepScanner +from socket_basics.socket_basics import count_blocking_alerts + + +class _DummyConnector: + def __init__(self, config: Config): + self.config = config + + +def _build_alert(path: str = 'index.js') -> dict: + return { + 'title': 'js-sql-injection', + 'severity': 'critical', + 'props': { + 'ruleId': 'js-sql-injection', + 'filePath': path, + 'startLine': 14, + 'endLine': 14, + }, + 'location': { + 'path': path, + 'start': 14, + 'end': 14, + }, + } + + +def test_parse_sast_ignore_overrides_supports_rule_and_exact_path(): + parsed = parse_sast_ignore_overrides( + 'js-sql-injection, js-sql-injection:./src/db/query.js' + ) + + assert parsed == [ + {'rule_id': 'js-sql-injection', 'path': None}, + {'rule_id': 'js-sql-injection', 'path': 'src/db/query.js'}, + ] + + +def test_parse_sast_ignore_overrides_skips_glob_paths(caplog): + parsed = parse_sast_ignore_overrides('js-sql-injection:src/**/*.js') + + assert parsed == [] + assert 'glob syntax' in caplog.text + + +def test_normalize_alert_ignores_rule_only_override(): + connector = _DummyConnector(Config({'workspace': '.', 'sast_ignore_overrides': 'js-sql-injection'})) + + alert = _normalize_alert(_build_alert(), connector=connector) + + assert alert['action'] == 'ignore' + + +def test_normalize_alert_ignores_matching_rule_and_path_override(): + connector = _DummyConnector( + Config({'workspace': '.', 'sast_ignore_overrides': 'js-sql-injection:index.js'}) + ) + + alert = _normalize_alert(_build_alert(), connector=connector) + + assert alert['action'] == 'ignore' + + +def test_normalize_alert_accepts_windows_style_override_paths(): + connector = _DummyConnector( + Config({'workspace': '.', 'sast_ignore_overrides': r'js-sql-injection:src\unsafe\demo.js'}) + ) + + alert = _normalize_alert(_build_alert('src/unsafe/demo.js'), connector=connector) + + assert alert['action'] == 'ignore' + + +def test_normalize_alert_does_not_ignore_non_matching_path_override(): + connector = _DummyConnector( + Config({'workspace': '.', 'sast_ignore_overrides': 'js-sql-injection:src/index.js'}) + ) + + alert = _normalize_alert(_build_alert(), connector=connector) + + assert alert['action'] == 'error' + + +def test_count_blocking_alerts_skips_ignored_findings(): + results = { + 'components': [ + {'id': 'ignored.js', 'alerts': [{**_build_alert('ignored.js'), 'action': 'ignore'}]}, + {'id': 'active.js', 'alerts': [{**_build_alert('active.js'), 'action': 'error'}]}, + ] + } + + assert count_blocking_alerts(results) == 1 + + +def test_opengrep_notifications_skip_ignored_findings(): + scanner = OpenGrepScanner(Config({'workspace': '.'})) + component = { + 'id': 'index.js', + 'qualifiers': {'scanner': 'opengrep', 'type': 'javascript'}, + 'alerts': [{**_build_alert(), 'action': 'ignore', 'subType': 'sast-javascript'}], + } + + notifications = scanner.generate_notifications([component]) + + assert notifications == {} From 050425af1107073d6851f9457231e795248c69a1 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:49:24 -0400 Subject: [PATCH 02/17] fix: skip ignored SAST findings in blocking logic Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- socket_basics/core/connector/normalizer.py | 12 ++++++++++++ socket_basics/socket_basics.py | 18 +++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/socket_basics/core/connector/normalizer.py b/socket_basics/core/connector/normalizer.py index 8837aa2..1e10d34 100644 --- a/socket_basics/core/connector/normalizer.py +++ b/socket_basics/core/connector/normalizer.py @@ -11,6 +11,7 @@ from typing import Any, Dict, List, Tuple import logging import os +from ..config import alert_matches_sast_ignore_override logger = logging.getLogger(__name__) @@ -38,6 +39,17 @@ def _normalize_alert(a: Dict[str, Any], connector: Any | None = None, default_ge a['severity'] = a['severity'].lower() # Minimal normalization: lowercase severity and ensure action exists + # Honor local SAST ignore overrides before deriving actions from severity. + try: + if connector and hasattr(connector, 'config') and hasattr(connector.config, 'get_sast_ignore_overrides'): + for override in connector.config.get_sast_ignore_overrides(): + if alert_matches_sast_ignore_override(a, override): + logger.debug("Alert matched sast_ignore_overrides entry %s", override) + a['action'] = 'ignore' + return a + except Exception: + logger.debug('Failed to check SAST ignore overrides for alert', exc_info=True) + # Check if this alert's rule is in the disabled rules list for any language # If so, set action to 'ignore' regardless of severity try: diff --git a/socket_basics/socket_basics.py b/socket_basics/socket_basics.py index a7f7f04..b10707d 100644 --- a/socket_basics/socket_basics.py +++ b/socket_basics/socket_basics.py @@ -54,6 +54,18 @@ logger = logging.getLogger(__name__) +def count_blocking_alerts(results: Dict[str, Any]) -> int: + """Count alerts that should fail the run.""" + blocking_alerts = 0 + for comp in results.get('components', []): + for alert in comp.get('alerts', []): + if (alert.get('action') or '').strip().lower() == 'ignore': + continue + if alert.get('severity') in ['high', 'critical']: + blocking_alerts += 1 + return blocking_alerts + + class SecurityScanner: """Main security scanning orchestrator using dynamic connectors""" @@ -456,11 +468,7 @@ def main(): logger.info(f"Total alerts: {total_alerts}") # Exit with non-zero code if high/critical issues found - high_critical_alerts = 0 - for comp in results.get('components', []): - for alert in comp.get('alerts', []): - if alert.get('severity') in ['high', 'critical']: - high_critical_alerts += 1 + high_critical_alerts = count_blocking_alerts(results) exit_code = 1 if high_critical_alerts > 0 else 0 if high_critical_alerts > 0: From 413174bc2a516b87413f25f024fa6e656d68e0f1 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:31:29 -0400 Subject: [PATCH 03/17] fix: strip common CI workspace prefixes from filepaths before matching Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- socket_basics/core/config.py | 59 ++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/socket_basics/core/config.py b/socket_basics/core/config.py index 6f20125..af3c737 100644 --- a/socket_basics/core/config.py +++ b/socket_basics/core/config.py @@ -15,8 +15,8 @@ logger = logging.getLogger(__name__) -def normalize_repo_relative_path(path_value: str | None) -> str | None: - """Normalize a repo-relative path to the POSIX form emitted by SAST alerts.""" +def _normalize_path_parts(path_value: str | None) -> List[str] | None: + """Normalize a path-like string into comparable POSIX-style path segments.""" if path_value is None: return None @@ -24,7 +24,6 @@ def normalize_repo_relative_path(path_value: str | None) -> str | None: if not path_str: return None - # Accept common local input styles but keep the final format strict. path_str = path_str.replace('\\', '/') while path_str.startswith('./'): path_str = path_str[2:] @@ -38,6 +37,60 @@ def normalize_repo_relative_path(path_value: str | None) -> str | None: return None normalized_parts.append(part) + return normalized_parts or None + + +def _get_workspace_prefix_candidates() -> List[List[str]]: + """Return normalized workspace roots from common CI systems and local cwd.""" + candidate_values: List[str] = [] + for env_var in ( + 'BITBUCKET_CLONE_DIR', + 'BUILD_SOURCESDIRECTORY', + 'BUILDKITE_BUILD_CHECKOUT_PATH', + 'CI_PROJECT_DIR', + 'CIRCLE_WORKING_DIRECTORY', + 'DRONE_WORKSPACE', + 'GITHUB_WORKSPACE', + 'SYSTEM_DEFAULTWORKINGDIRECTORY', + 'WORKSPACE', + ): + env_value = os.getenv(env_var) + if env_value: + candidate_values.append(env_value) + + try: + candidate_values.append(os.getcwd()) + except Exception: + pass + + normalized_candidates: List[List[str]] = [] + seen: set[tuple[str, ...]] = set() + for value in candidate_values: + parts = _normalize_path_parts(value) + if not parts: + continue + parts_key = tuple(parts) + if parts_key in seen: + continue + seen.add(parts_key) + normalized_candidates.append(parts) + + # Check longer, more specific prefixes first. + normalized_candidates.sort(key=len, reverse=True) + return normalized_candidates + + +def normalize_repo_relative_path(path_value: str | None) -> str | None: + """Normalize a repo-relative path to the POSIX form emitted by SAST alerts.""" + normalized_parts = _normalize_path_parts(path_value) + if not normalized_parts: + return None + + for workspace_parts in _get_workspace_prefix_candidates(): + if len(normalized_parts) > len(workspace_parts) and normalized_parts[:len(workspace_parts)] == workspace_parts: + normalized_parts = normalized_parts[len(workspace_parts):] + break + normalized = '/'.join(normalized_parts) return normalized or None From 73fa3e6933b7e495655e6683bf4f51dd86ff5a6d Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:32:01 -0400 Subject: [PATCH 04/17] chore: add tests for GHA and common CI checkout path routes Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- tests/test_sast_ignore_overrides.py | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_sast_ignore_overrides.py b/tests/test_sast_ignore_overrides.py index 0bb5678..506fb1d 100644 --- a/tests/test_sast_ignore_overrides.py +++ b/tests/test_sast_ignore_overrides.py @@ -1,5 +1,6 @@ from socket_basics.core.config import ( Config, + normalize_repo_relative_path, parse_sast_ignore_overrides, ) from socket_basics.core.connector.normalizer import _normalize_alert @@ -76,6 +77,47 @@ def test_normalize_alert_accepts_windows_style_override_paths(): assert alert['action'] == 'ignore' +def test_normalize_repo_relative_path_strips_github_actions_workspace_prefix(monkeypatch): + monkeypatch.setenv('GITHUB_WORKSPACE', '/github/workspace') + + assert normalize_repo_relative_path('github/workspace/index.js') == 'index.js' + + +def test_normalize_repo_relative_path_strips_gitlab_workspace_prefix(monkeypatch): + monkeypatch.setenv('CI_PROJECT_DIR', '/builds/acme/sample-repo') + + assert normalize_repo_relative_path('/builds/acme/sample-repo/src/index.js') == 'src/index.js' + + +def test_normalize_repo_relative_path_strips_bitbucket_workspace_prefix(monkeypatch): + monkeypatch.setenv('BITBUCKET_CLONE_DIR', '/opt/atlassian/pipelines/agent/build') + + assert normalize_repo_relative_path('/opt/atlassian/pipelines/agent/build/index.js') == 'index.js' + + +def test_normalize_repo_relative_path_strips_buildkite_workspace_prefix(monkeypatch): + monkeypatch.setenv('BUILDKITE_BUILD_CHECKOUT_PATH', '/var/lib/buildkite-agent/builds/agent-1/org/repo') + + assert normalize_repo_relative_path( + '/var/lib/buildkite-agent/builds/agent-1/org/repo/app/index.js' + ) == 'app/index.js' + + +def test_normalize_alert_strips_github_actions_workspace_prefix(monkeypatch): + monkeypatch.setenv('GITHUB_WORKSPACE', '/github/workspace') + + connector = _DummyConnector( + Config({'workspace': '.', 'sast_ignore_overrides': 'js-sql-injection:index.js'}) + ) + + alert = _normalize_alert( + _build_alert('github/workspace/index.js'), + connector=connector, + ) + + assert alert['action'] == 'ignore' + + def test_normalize_alert_does_not_ignore_non_matching_path_override(): connector = _DummyConnector( Config({'workspace': '.', 'sast_ignore_overrides': 'js-sql-injection:src/index.js'}) From 7a6ac0df63ff8e76531cf10b0e8e3a09bd0292b1 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:27:16 -0400 Subject: [PATCH 05/17] docs: describe new low severity label option Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- action.yml | 5 +++++ docs/github-pr-comment-guide.md | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 6cec1e2..fcdc9c6 100644 --- a/action.yml +++ b/action.yml @@ -98,6 +98,7 @@ runs: INPUT_PR_LABEL_CRITICAL: ${{ inputs.pr_label_critical }} INPUT_PR_LABEL_HIGH: ${{ inputs.pr_label_high }} INPUT_PR_LABEL_MEDIUM: ${{ inputs.pr_label_medium }} + INPUT_PR_LABEL_LOW: ${{ inputs.pr_label_low }} inputs: workspace: @@ -456,6 +457,10 @@ inputs: description: "Label name for medium severity findings" required: false default: "security: medium" + pr_label_low: + description: "Label name for low severity findings" + required: false + default: "security: low" branding: icon: "shield" diff --git a/docs/github-pr-comment-guide.md b/docs/github-pr-comment-guide.md index 0cd949d..5351366 100644 --- a/docs/github-pr-comment-guide.md +++ b/docs/github-pr-comment-guide.md @@ -236,6 +236,7 @@ Automatically tag PRs with severity-based labels **and matching colors**. - `security: critical` 🔴 - Red (`#D73A4A`) - `security: high` 🟠 - Orange (`#D93F0B`) - `security: medium` 🟡 - Yellow (`#FBCA04`) +- `security: low` ⚪ - Light gray (`#E4E4E4`) **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: @@ -246,7 +247,8 @@ pr_label_high: 'security-high' # Gets orange color automatically ``` **How it works:** -- First scan checks for critical → high → medium (highest severity wins) +- Each run keeps only the current highest-severity managed PR label: critical → high → medium → low +- Stale managed severity labels from earlier runs are removed automatically - Labels are created automatically if they don't exist - Existing labels are not modified (preserves your customizations) - Requires a token with `repo` scope to create new labels; without it, label creation may fail (comments still post) @@ -257,6 +259,7 @@ pr_labels_enabled: 'true' pr_label_critical: 'vulnerability: critical' pr_label_high: 'vulnerability: high' pr_label_medium: 'vulnerability: medium' +pr_label_low: 'vulnerability: low' ``` **Disable:** @@ -295,6 +298,7 @@ The logo is a 32px PNG rendered at 24x24 for retina-crisp display, with a transp | `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 | +| `pr_label_low` | `"security: low"` | string | Label name for low findings | ### Configuration Methods From db6d25a5692bf6803a91ca41925f192e6e9e656c Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:27:47 -0400 Subject: [PATCH 06/17] chore: add low severity config to notifications Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- socket_basics/notifications.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/socket_basics/notifications.yaml b/socket_basics/notifications.yaml index a6eec60..02c2e3d 100644 --- a/socket_basics/notifications.yaml +++ b/socket_basics/notifications.yaml @@ -144,6 +144,12 @@ notifiers: type: str default: "security: medium" description: "Label name for medium severity findings" + - name: pr_label_low + option: --pr-label-low + env_variable: INPUT_PR_LABEL_LOW + type: str + default: "security: low" + description: "Label name for low severity findings" msteams: module_path: "socket_basics.core.notification.ms_teams_notifier" From 6e63e27f5909648c0a130a583b86f19897681b52 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:29:24 -0400 Subject: [PATCH 07/17] fix: check severity label on reruns, improve error logging, add tests Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- .../core/notification/github_pr_notifier.py | 175 ++++++++++++++---- tests/test_github_pr_notifier.py | 89 +++++++++ 2 files changed, 229 insertions(+), 35 deletions(-) create mode 100644 tests/test_github_pr_notifier.py diff --git a/socket_basics/core/notification/github_pr_notifier.py b/socket_basics/core/notification/github_pr_notifier.py index 555d03e..d2f6ebe 100644 --- a/socket_basics/core/notification/github_pr_notifier.py +++ b/socket_basics/core/notification/github_pr_notifier.py @@ -1,5 +1,6 @@ from typing import Any, Dict, List, Optional import logging +from urllib.parse import quote from socket_basics.core.notification.base import BaseNotifier from socket_basics.core.config import get_github_token, get_github_repository, get_github_pr_number @@ -118,8 +119,39 @@ def notify(self, facts: Dict[str, Any]) -> None: # 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) + self._reconcile_pr_labels(pr_number, labels) + def _managed_pr_label_config(self) -> Dict[str, str]: + """Return the managed severity label names configured for PRs.""" + return { + 'critical': self.config.get('pr_label_critical', 'security: critical'), + 'high': self.config.get('pr_label_high', 'security: high'), + 'medium': self.config.get('pr_label_medium', 'security: medium'), + 'low': self.config.get('pr_label_low', 'security: low'), + } + + def _get_label_color_info(self, label: str) -> Optional[tuple[str, str]]: + """Infer color/description for managed or custom severity labels.""" + label_colors = { + self.config.get('pr_label_critical', 'security: critical'): ('D73A4A', 'Critical security vulnerabilities'), + self.config.get('pr_label_high', 'security: high'): ('D93F0B', 'High severity security issues'), + self.config.get('pr_label_medium', 'security: medium'): ('FBCA04', 'Medium severity security issues'), + self.config.get('pr_label_low', 'security: low'): ('E4E4E4', 'Low severity security issues'), + } + color_info = label_colors.get(label) + if color_info: + return color_info + + label_lower = label.lower() + if 'critical' in label_lower: + return ('D73A4A', 'Critical security vulnerabilities') + if 'high' in label_lower: + return ('D93F0B', 'High severity security issues') + if 'medium' in label_lower: + return ('FBCA04', 'Medium severity security issues') + if 'low' in label_lower: + return ('E4E4E4', 'Low severity security issues') + return None + def _send_pr_comment(self, facts: Dict[str, Any], title: str, content: str) -> None: """Send a single PR comment with title and content.""" @@ -423,19 +455,93 @@ def _ensure_label_exists_with_color(self, label_name: str, color: str, descripti 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) + logger.warning( + 'GithubPRNotifier: failed to create label "%s": %s %s', + label_name, + create_resp.status_code, + create_resp.text[:200], + ) return False else: - logger.warning('GithubPRNotifier: unexpected response checking label: %s', resp.status_code) + logger.warning( + 'GithubPRNotifier: unexpected response checking label "%s": %s %s', + label_name, + resp.status_code, + resp.text[:200], + ) return False except Exception as e: logger.debug('GithubPRNotifier: exception ensuring label exists: %s', e) return False + def _ensure_pr_labels_exist(self, labels: List[str]) -> None: + """Ensure desired labels exist in the repository with appropriate colors.""" + for label in labels: + color_info = self._get_label_color_info(label) + if color_info: + color, description = color_info + self._ensure_label_exists_with_color(label, color, description) + + def _get_current_pr_label_names(self, pr_number: int) -> List[str]: + """Fetch current label names for the PR.""" + if not self.repository: + return [] + + 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" + resp = requests.get(url, headers=headers, timeout=10) + if resp.status_code == 200: + payload = resp.json() + return [label.get('name') for label in payload if isinstance(label, dict) and label.get('name')] + logger.warning( + 'GithubPRNotifier: failed to fetch current labels for PR %s: %s %s', + pr_number, + resp.status_code, + resp.text[:200], + ) + except Exception as e: + logger.error('GithubPRNotifier: exception fetching current labels: %s', e) + return [] + + def _remove_pr_label(self, pr_number: int, label: str) -> bool: + """Remove a label from a PR.""" + if not self.repository or not label: + return False + + try: + import requests + headers = { + 'Authorization': f'token {self.token}', + 'Accept': 'application/vnd.github.v3+json' + } + encoded_label = quote(label, safe='') + url = f"{self.api_base}/repos/{self.repository}/issues/{pr_number}/labels/{encoded_label}" + resp = requests.delete(url, headers=headers, timeout=10) + if resp.status_code == 200: + logger.info('GithubPRNotifier: removed label from PR %s: %s', pr_number, label) + return True + if resp.status_code == 404: + logger.debug('GithubPRNotifier: label %s already absent from PR %s', label, pr_number) + return True + logger.warning( + 'GithubPRNotifier: failed to remove label "%s" from PR %s: %s %s', + label, + pr_number, + resp.status_code, + resp.text[:200], + ) + except Exception as e: + logger.error('GithubPRNotifier: exception removing label %s: %s', label, e) + return False + def _add_pr_labels(self, pr_number: int, labels: List[str]) -> bool: - """Add labels to a PR, ensuring they exist with appropriate colors. + """Add missing labels to a PR. Args: pr_number: PR number @@ -447,34 +553,6 @@ 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 = { @@ -490,12 +568,33 @@ def _add_pr_labels(self, pr_number: int, labels: List[str]) -> bool: 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) + logger.warning('GithubPRNotifier: failed to add labels: %s %s', resp.status_code, resp.text[:200]) return False except Exception as e: logger.error('GithubPRNotifier: exception adding labels: %s', e) return False + def _reconcile_pr_labels(self, pr_number: int, desired_labels: List[str]) -> bool: + """Reconcile managed severity labels on the PR to match the latest run.""" + managed_labels = set(filter(None, self._managed_pr_label_config().values())) + current_labels = set(self._get_current_pr_label_names(pr_number)) + desired_label_set = set(filter(None, desired_labels)) + + stale_labels = sorted(label for label in current_labels if label in managed_labels and label not in desired_label_set) + labels_to_add = sorted(label for label in desired_label_set if label not in current_labels) + + success = True + for label in stale_labels: + success = self._remove_pr_label(pr_number, label) and success + + if labels_to_add: + self._ensure_pr_labels_exist(labels_to_add) + success = self._add_pr_labels(pr_number, labels_to_add) and success + + if not stale_labels and not labels_to_add: + logger.info('GithubPRNotifier: PR %s severity labels already up to date', pr_number) + return success + def _determine_pr_labels(self, notifications: List[Dict[str, Any]]) -> List[str]: """Determine which labels to add based on notifications. @@ -517,6 +616,7 @@ def _determine_pr_labels(self, notifications: List[Dict[str, Any]]) -> List[str] 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) + low_match = re.search(r'Low:\s*(\d+)', content) if critical_match and int(critical_match.group(1)) > 0: severities_found.add('critical') @@ -524,6 +624,8 @@ def _determine_pr_labels(self, notifications: List[Dict[str, Any]]) -> List[str] severities_found.add('high') if medium_match and int(medium_match.group(1)) > 0: severities_found.add('medium') + if low_match and int(low_match.group(1)) > 0: + severities_found.add('low') # Map severities to label names (using configurable labels) labels = [] @@ -536,5 +638,8 @@ def _determine_pr_labels(self, notifications: List[Dict[str, Any]]) -> List[str] elif 'medium' in severities_found: label_name = self.config.get('pr_label_medium', 'security: medium') labels.append(label_name) + elif 'low' in severities_found: + label_name = self.config.get('pr_label_low', 'security: low') + labels.append(label_name) return labels \ No newline at end of file diff --git a/tests/test_github_pr_notifier.py b/tests/test_github_pr_notifier.py new file mode 100644 index 0000000..1d374a2 --- /dev/null +++ b/tests/test_github_pr_notifier.py @@ -0,0 +1,89 @@ +from socket_basics.core.notification.github_pr_notifier import GithubPRNotifier + + +def _notification(summary: str) -> dict: + return {'title': 'Socket SAST JavaScript', 'content': summary} + + +def test_determine_pr_labels_prefers_highest_current_severity(): + notifier = GithubPRNotifier( + { + 'repository': 'SocketDev/socket-basics', + 'pr_label_critical': 'security: critical', + 'pr_label_high': 'security: high', + 'pr_label_medium': 'security: medium', + 'pr_label_low': 'security: low', + } + ) + + labels = notifier._determine_pr_labels( + [_notification('Critical: 0 | High: 1 | Medium: 2 | Low: 3')] + ) + + assert labels == ['security: high'] + + +def test_determine_pr_labels_supports_low_severity(): + notifier = GithubPRNotifier( + { + 'repository': 'SocketDev/socket-basics', + 'pr_label_low': 'security: low', + } + ) + + labels = notifier._determine_pr_labels( + [_notification('Critical: 0 | High: 0 | Medium: 0 | Low: 2')] + ) + + assert labels == ['security: low'] + + +def test_reconcile_pr_labels_replaces_stale_managed_severity(monkeypatch): + notifier = GithubPRNotifier( + { + 'repository': 'SocketDev/socket-basics', + 'pr_label_critical': 'security: critical', + 'pr_label_high': 'security: high', + 'pr_label_medium': 'security: medium', + 'pr_label_low': 'security: low', + } + ) + + removed: list[str] = [] + added: list[str] = [] + ensured: list[str] = [] + + monkeypatch.setattr(notifier, '_get_current_pr_label_names', lambda pr_number: ['security: critical', 'team: backend']) + monkeypatch.setattr(notifier, '_remove_pr_label', lambda pr_number, label: removed.append(label) or True) + monkeypatch.setattr(notifier, '_ensure_pr_labels_exist', lambda labels: ensured.extend(labels)) + monkeypatch.setattr(notifier, '_add_pr_labels', lambda pr_number, labels: added.extend(labels) or True) + + success = notifier._reconcile_pr_labels(123, ['security: medium']) + + assert success is True + assert removed == ['security: critical'] + assert ensured == ['security: medium'] + assert added == ['security: medium'] + + +def test_reconcile_pr_labels_clears_managed_labels_when_none_desired(monkeypatch): + notifier = GithubPRNotifier( + { + 'repository': 'SocketDev/socket-basics', + 'pr_label_critical': 'security: critical', + 'pr_label_high': 'security: high', + 'pr_label_medium': 'security: medium', + 'pr_label_low': 'security: low', + } + ) + + removed: list[str] = [] + monkeypatch.setattr(notifier, '_get_current_pr_label_names', lambda pr_number: ['security: high', 'docs']) + monkeypatch.setattr(notifier, '_remove_pr_label', lambda pr_number, label: removed.append(label) or True) + monkeypatch.setattr(notifier, '_ensure_pr_labels_exist', lambda labels: (_ for _ in ()).throw(AssertionError('should not ensure labels'))) + monkeypatch.setattr(notifier, '_add_pr_labels', lambda pr_number, labels: (_ for _ in ()).throw(AssertionError('should not add labels'))) + + success = notifier._reconcile_pr_labels(123, []) + + assert success is True + assert removed == ['security: high'] From bbe170ff2af07568df4ae1ee1590a39fd16eadaa Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:41:48 -0400 Subject: [PATCH 08/17] fix: clear stale labels when findings are downgraded, ignored, or resolved Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- .../core/notification/github_pr_notifier.py | 14 +++++++++----- tests/test_github_pr_notifier.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/socket_basics/core/notification/github_pr_notifier.py b/socket_basics/core/notification/github_pr_notifier.py index d2f6ebe..3b9cdad 100644 --- a/socket_basics/core/notification/github_pr_notifier.py +++ b/socket_basics/core/notification/github_pr_notifier.py @@ -36,14 +36,11 @@ def __init__(self, params: Dict[str, Any] | None = None): def notify(self, facts: Dict[str, Any]) -> None: notifications = facts.get('notifications', []) or [] + labels_enabled = self.config.get('pr_labels_enabled', True) if not isinstance(notifications, list): logger.error('GithubPRNotifier: only supports new format - list of dicts with title/content') return - - if not notifications: - logger.info('GithubPRNotifier: no notifications present; skipping') - return # Get full scan URL if available and store it for use in truncation self.full_scan_url = facts.get('full_scan_html_url') @@ -58,6 +55,13 @@ def notify(self, facts: Dict[str, Any]) -> None: logger.warning('GithubPRNotifier: skipping invalid notification item: %s', type(item)) if not valid_notifications: + if labels_enabled: + pr_number = self._get_pr_number() + if pr_number: + self._reconcile_pr_labels(pr_number, []) + else: + logger.warning('GithubPRNotifier: unable to determine PR number for label reconciliation') + logger.info('GithubPRNotifier: no notifications present; skipping comments') return # Get PR number for current branch @@ -117,7 +121,7 @@ def notify(self, facts: Dict[str, Any]) -> None: 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: + if labels_enabled and pr_number: labels = self._determine_pr_labels(valid_notifications) self._reconcile_pr_labels(pr_number, labels) def _managed_pr_label_config(self) -> Dict[str, str]: diff --git a/tests/test_github_pr_notifier.py b/tests/test_github_pr_notifier.py index 1d374a2..cc82b4c 100644 --- a/tests/test_github_pr_notifier.py +++ b/tests/test_github_pr_notifier.py @@ -87,3 +87,20 @@ def test_reconcile_pr_labels_clears_managed_labels_when_none_desired(monkeypatch assert success is True assert removed == ['security: high'] + + +def test_notify_reconciles_labels_even_when_notifications_are_empty(monkeypatch): + notifier = GithubPRNotifier( + { + 'repository': 'SocketDev/socket-basics', + 'pr_labels_enabled': True, + } + ) + + reconciled: list[tuple[int, list[str]]] = [] + monkeypatch.setattr(notifier, '_get_pr_number', lambda: 123) + monkeypatch.setattr(notifier, '_reconcile_pr_labels', lambda pr_number, labels: reconciled.append((pr_number, labels)) or True) + + notifier.notify({'notifications': []}) + + assert reconciled == [(123, [])] From bd031a3a7ff89892fb7c9493407daeb6d7ac3daf Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:56:11 -0400 Subject: [PATCH 09/17] fix: reconcile severity labels on all-clear reruns Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- socket_basics/core/notification/manager.py | 9 ++++++++ tests/test_notification_manager_github_pr.py | 22 ++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/test_notification_manager_github_pr.py diff --git a/socket_basics/core/notification/manager.py b/socket_basics/core/notification/manager.py index d6e8dcc..7063f06 100644 --- a/socket_basics/core/notification/manager.py +++ b/socket_basics/core/notification/manager.py @@ -510,6 +510,15 @@ def _alert_group(alert: Dict[str, Any], comp: Dict[str, Any]) -> str: notifier_data = per_notifier_notifications[notification_key] notifier_facts['notifications'] = notifier_data logger.debug('Using pre-formatted data for notifier %s: %s items', notifier_name, len(notifier_data) if isinstance(notifier_data, list) else 1) + elif notification_key == 'github_pr': + # GitHub PR label reconciliation still needs to run on "all clear" + # reruns where there are no current notification sections. + notifier_facts['notifications'] = [] + logger.debug( + 'No pre-formatted data found for notifier %s (key: %s); passing empty notifications for label reconciliation', + notifier_name, + notification_key, + ) else: # No pre-formatted data available - skip this notifier to avoid sending wrong format logger.debug('No pre-formatted data found for notifier %s (key: %s), skipping to avoid format mismatch', notifier_name, notification_key) diff --git a/tests/test_notification_manager_github_pr.py b/tests/test_notification_manager_github_pr.py new file mode 100644 index 0000000..cc77309 --- /dev/null +++ b/tests/test_notification_manager_github_pr.py @@ -0,0 +1,22 @@ +from socket_basics.core.notification.manager import NotificationManager + + +class _DummyGithubPrNotifier: + name = "github_pr" + + def __init__(self): + self.payloads = [] + + def notify(self, facts): + self.payloads.append(facts) + + +def test_notify_all_passes_empty_notifications_to_github_pr_for_all_clear(): + notifier = _DummyGithubPrNotifier() + nm = NotificationManager({}, app_config={"repo": "SocketDev/socket-basics"}) + nm.notifiers = [notifier] + + nm.notify_all({"components": [], "notifications": {}}) + + assert len(notifier.payloads) == 1 + assert notifier.payloads[0]["notifications"] == [] From 34b732f96a87522cd2dddea8a52c511ab7d56a9e Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:55:30 -0400 Subject: [PATCH 10/17] fix: update existing PR comments to add all-clear message Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- .../core/notification/github_pr_notifier.py | 63 +++++++++++++++++++ tests/test_github_pr_notifier.py | 32 ++++++++++ 2 files changed, 95 insertions(+) diff --git a/socket_basics/core/notification/github_pr_notifier.py b/socket_basics/core/notification/github_pr_notifier.py index 3b9cdad..87f988c 100644 --- a/socket_basics/core/notification/github_pr_notifier.py +++ b/socket_basics/core/notification/github_pr_notifier.py @@ -59,6 +59,7 @@ def notify(self, facts: Dict[str, Any]) -> None: pr_number = self._get_pr_number() if pr_number: self._reconcile_pr_labels(pr_number, []) + self._replace_existing_sections_with_all_clear(pr_number) else: logger.warning('GithubPRNotifier: unable to determine PR number for label reconciliation') logger.info('GithubPRNotifier: no notifications present; skipping comments') @@ -288,6 +289,25 @@ def _extract_section_markers(self, content: str) -> Optional[Dict[str, str]]: return None + def _extract_all_section_types(self, comment_body: str) -> List[str]: + """Extract all managed section markers from a comment body.""" + import re + + pattern = r'' + return re.findall(pattern, comment_body or '') + + def _extract_section_title(self, section_content: str) -> str: + """Extract the display title from a wrapped PR comment section.""" + import re + + for line in (section_content or '').splitlines(): + stripped = line.strip() + if stripped.startswith('## '): + title = stripped[3:].strip() + title = re.sub(r']+>\s*', '', title).strip() + return title or 'Socket Security' + return 'Socket Security' + def _find_comment_with_section(self, comments: List[Dict[str, Any]], section_type: str) -> Optional[Dict[str, Any]]: """Find an existing comment that contains the given section type.""" import re @@ -313,6 +333,49 @@ def _update_section_in_comment(self, comment_body: str, section_type: str, new_s return updated_body + def _build_all_clear_section(self, section_type: str, existing_section_content: str) -> str: + """Build an all-clear replacement for an existing managed section.""" + from socket_basics.core.notification import github_pr_helpers as helpers + + title = self._extract_section_title(existing_section_content) + body = "✅ Socket Basics found no active findings in the latest run." + return helpers.wrap_pr_comment_section(section_type, title, body, self.full_scan_url) + + def _replace_existing_sections_with_all_clear(self, pr_number: int) -> None: + """Rewrite existing managed PR comment sections to an all-clear state.""" + existing_comments = self._get_pr_comments(pr_number) + for comment in existing_comments: + original_body = comment.get('body', '') + if not original_body: + continue + + updated_body = original_body + changed = False + for section_type in self._extract_all_section_types(original_body): + section_match = self._extract_section_markers(updated_body) + if not section_match or section_match.get('type') != section_type: + import re + pattern = rf'.*?' + match = re.search(pattern, updated_body, re.DOTALL) + if not match: + continue + section_content = match.group(0) + else: + section_content = section_match['content'] + + all_clear_section = self._build_all_clear_section(section_type, section_content) + next_body = self._update_section_in_comment(updated_body, section_type, all_clear_section) + if next_body != updated_body: + updated_body = next_body + changed = True + + if changed: + success = self._update_comment(pr_number, comment['id'], updated_body) + if success: + logger.info('GithubPRNotifier: updated existing comment %s to all-clear state', comment['id']) + else: + logger.error('GithubPRNotifier: failed to update comment %s to all-clear state', comment['id']) + def _truncate_comment_if_needed(self, comment_body: str, full_scan_url: Optional[str] = None) -> str: """Truncate comment if it exceeds GitHub's character limit. diff --git a/tests/test_github_pr_notifier.py b/tests/test_github_pr_notifier.py index cc82b4c..c5a32d8 100644 --- a/tests/test_github_pr_notifier.py +++ b/tests/test_github_pr_notifier.py @@ -104,3 +104,35 @@ def test_notify_reconciles_labels_even_when_notifications_are_empty(monkeypatch) notifier.notify({'notifications': []}) assert reconciled == [(123, [])] + + +def test_notify_rewrites_existing_section_to_all_clear_when_notifications_are_empty(monkeypatch): + notifier = GithubPRNotifier( + { + 'repository': 'SocketDev/socket-basics', + 'pr_labels_enabled': True, + } + ) + + comment_body = """ +## Socket SAST JavaScript + +### Summary +🟡 Medium: 1 +""" + updated_bodies: list[str] = [] + + monkeypatch.setattr(notifier, '_get_pr_number', lambda: 123) + monkeypatch.setattr(notifier, '_reconcile_pr_labels', lambda pr_number, labels: True) + monkeypatch.setattr(notifier, '_get_pr_comments', lambda pr_number: [{'id': 99, 'body': comment_body}]) + monkeypatch.setattr( + notifier, + '_update_comment', + lambda pr_number, comment_id, body: updated_bodies.append(body) or True, + ) + + notifier.notify({'notifications': []}) + + assert len(updated_bodies) == 1 + assert 'No active findings remain for this scanner in the latest run.' in updated_bodies[0] + assert '' in updated_bodies[0] From 9e66ea2f21e985d6956ba455a53bfdca1138b149 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:57:50 -0400 Subject: [PATCH 11/17] fix: update phrasing in test assertion for PR comment body Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- tests/test_github_pr_notifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_github_pr_notifier.py b/tests/test_github_pr_notifier.py index c5a32d8..255795f 100644 --- a/tests/test_github_pr_notifier.py +++ b/tests/test_github_pr_notifier.py @@ -134,5 +134,5 @@ def test_notify_rewrites_existing_section_to_all_clear_when_notifications_are_em notifier.notify({'notifications': []}) assert len(updated_bodies) == 1 - assert 'No active findings remain for this scanner in the latest run.' in updated_bodies[0] + assert 'Socket Basics found no active findings in the latest run.' in updated_bodies[0] assert '' in updated_bodies[0] From 80001671f9ff4dc7fc945d656e20852680cbd85b Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:08:11 -0400 Subject: [PATCH 12/17] docs: update docs to reflect new override and PR comment lifecycle capabilities Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- docs/github-action.md | 3 ++- docs/github-pr-comment-guide.md | 17 +++++++++++++++++ docs/parameters.md | 3 +++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/github-action.md b/docs/github-action.md index 64f5dcf..4abaf08 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -639,7 +639,7 @@ jobs: javascript_sast_enabled: 'true' javascript_enabled_rules: 'eval-usage,prototype-pollution' - # Ignore a specific SAST rule globally or for one exact file + # Ignore one or more SAST rules globally or for exact repo-relative files sast_ignore_overrides: 'js-sql-injection:index.js' ``` @@ -651,6 +651,7 @@ Examples: - `js-sql-injection` - `js-sql-injection:index.js` - `js-sql-injection:src/unsafe/demo.js` +- `js-express-async-no-error-handler,js-sql-injection:index.js,js-missing-helmet` Notes: - Paths must be exact repo-relative paths using `/` separators after normalization. diff --git a/docs/github-pr-comment-guide.md b/docs/github-pr-comment-guide.md index 5351366..1466612 100644 --- a/docs/github-pr-comment-guide.md +++ b/docs/github-pr-comment-guide.md @@ -249,6 +249,7 @@ pr_label_high: 'security-high' # Gets orange color automatically **How it works:** - Each run keeps only the current highest-severity managed PR label: critical → high → medium → low - Stale managed severity labels from earlier runs are removed automatically +- If a later run has no active findings, the managed severity label is removed - Labels are created automatically if they don't exist - Existing labels are not modified (preserves your customizations) - Requires a token with `repo` scope to create new labels; without it, label creation may fail (comments still post) @@ -283,6 +284,22 @@ The logo is a 32px PNG rendered at 24x24 for retina-crisp display, with a transp --- +### 9. All-Clear Comment Updates + +When a later Socket Basics run no longer has active findings for a previously-reported scanner section, the existing PR comment section is updated in place instead of being left stale or deleted. + +**Behavior:** +- Existing Socket-managed sections are preserved for auditability +- Stale findings content is replaced with a short all-clear message +- This keeps the PR history readable while making it obvious that the latest run is clean + +**Example all-clear message:** +```text +✅ Socket Basics found no active findings in the latest run. +``` + +--- + ## 📋 Configuration Reference ### All Options diff --git a/docs/parameters.md b/docs/parameters.md index c422a43..5b163be 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -253,6 +253,9 @@ socket-basics --javascript --sast-ignore-overrides "js-sql-injection" # Ignore a rule only for one exact repo-relative file socket-basics --javascript --sast-ignore-overrides "js-sql-injection:index.js" + +# Mix rule-only and rule+path overrides in one comma-separated list +socket-basics --javascript --sast-ignore-overrides "js-express-async-no-error-handler,js-sql-injection:index.js,js-missing-helmet" ``` Notes: From d599a9f9624e5903d466487482cf1fbe6b600a5d Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:19:58 -0400 Subject: [PATCH 13/17] chore: revert action manifest back to proper GHCR image path Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- action.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/action.yml b/action.yml index fcdc9c6..b109084 100644 --- a/action.yml +++ b/action.yml @@ -4,8 +4,7 @@ author: "Socket" runs: using: "docker" - # TODO: revert to the published GHCR image before merging/releasing. - image: "Dockerfile" + image: "docker://ghcr.io/socketdev/socket-basics:2.0.2" env: # Core GitHub variables (these are automatically available, but we explicitly pass GITHUB_TOKEN) GITHUB_TOKEN: ${{ inputs.github_token }} From 674c4a646f523d0570af8bec6ec50b68fadf66c4 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:59:12 -0400 Subject: [PATCH 14/17] fix: add diagnostics, logging for invalid SAST override filepaths Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- docs/github-action.md | 17 +++++++++++ docs/parameters.md | 4 +++ socket_basics/core/config.py | 34 +++++++++++++++++++++- socket_basics/core/connector/normalizer.py | 2 ++ tests/test_sast_ignore_overrides.py | 30 +++++++++++++++++++ 5 files changed, 86 insertions(+), 1 deletion(-) diff --git a/docs/github-action.md b/docs/github-action.md index 4abaf08..f467b5a 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -657,6 +657,9 @@ Notes: - Paths must be exact repo-relative paths using `/` separators after normalization. - Windows-style input such as `src\\unsafe\\demo.js` is accepted and normalized automatically. - Globs and directory-prefix matching are not supported in this first version. +- A `rule_id:path` entry is an exact `rule_id AND path` match. If the path does not match, Socket Basics will not fall back to a rule-only ignore. +- Broad dashboard rule disables such as `_disabled_rules` still ignore that rule everywhere in the repo. If both are configured, the broad disabled-rule behavior can make it look like a narrow path override matched when it did not. +- In `.socket.facts.json`, ignored alerts include `actionReason` so you can see whether the ignore came from `sast_ignore_override` or `disabled_rule`. ## Configuration Reference @@ -769,6 +772,20 @@ permissions: 2. Check that `socket_org` and `socket_security_api_key` are set correctly 3. Confirm API key has required permissions in Socket Dashboard +### `sast_ignore_overrides` Seems Too Broad + +**Problem:** A `rule_id:path` override appears to ignore findings outside the specified file. + +**Likely cause:** The rule is also disabled more broadly in dashboard settings or other config through `_disabled_rules`. + +**How to confirm:** +1. Open the generated `.socket.facts.json` +2. Find the ignored alert and inspect `actionReason` +3. `actionReason: "sast_ignore_override"` means the exact path override matched +4. `actionReason: "disabled_rule"` means the finding was ignored by a broad rule disable instead + +**Additional signal:** If the configured path does not exist under the workspace, Socket Basics logs a warning and does not fall back to rule-only matching. + ### High Memory Usage **Problem:** Action runs out of memory. diff --git a/docs/parameters.md b/docs/parameters.md index 5b163be..deb0268 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -262,6 +262,10 @@ Notes: - Paths must be exact repo-relative paths. - Paths are normalized to forward-slash form, so Windows-style input such as `src\\unsafe\\demo.js` is accepted. - Globs and directory-prefix matching are not supported in this first version. +- A `rule_id:path` entry uses exact `rule_id AND path` matching. A bad path does not degrade into a rule-only ignore. +- If the configured path does not exist under the current workspace, Socket Basics logs a warning to help catch typos or copied paths from another repo. +- If the same rule is also disabled via `-disabled-rules` or dashboard policy, that broader ignore still applies across the repo. +- Ignored alerts in `.socket.facts.json` include `actionReason` so you can distinguish `sast_ignore_override` from `disabled_rule`. ### `--opengrep-notify OPENGREP_NOTIFY` Notification method for OpenGrep SAST results (e.g., console, slack). diff --git a/socket_basics/core/config.py b/socket_basics/core/config.py index af3c737..6c828be 100644 --- a/socket_basics/core/config.py +++ b/socket_basics/core/config.py @@ -271,7 +271,39 @@ def get_sast_ignore_overrides(self) -> List[Dict[str, str | None]]: """Return parsed SAST ignore overrides from config.""" if not hasattr(self, '_sast_ignore_overrides_cache'): raw_value = self.get('sast_ignore_overrides', '') - self._sast_ignore_overrides_cache = parse_sast_ignore_overrides(raw_value) + overrides = parse_sast_ignore_overrides(raw_value) + + workspace_value = self.get('workspace') or os.getcwd() + try: + workspace_root = Path(str(workspace_value)).expanduser() + except Exception: + workspace_root = Path(os.getcwd()) + + for override in overrides: + override_path = override.get('path') + if not override_path: + continue + + try: + candidate = workspace_root.joinpath(*str(override_path).split('/')) + if not candidate.exists(): + logger.warning( + "SAST ignore override path %r for rule %r does not exist under workspace %s; " + "exact path overrides require a repo-relative file path and will not fall back to " + "rule-only matching.", + override_path, + override.get('rule_id'), + workspace_root, + ) + except Exception: + logger.debug( + "Failed to validate SAST ignore override path %r under workspace %r", + override_path, + workspace_value, + exc_info=True, + ) + + self._sast_ignore_overrides_cache = overrides return self._sast_ignore_overrides_cache @property diff --git a/socket_basics/core/connector/normalizer.py b/socket_basics/core/connector/normalizer.py index 1e10d34..cd8c1c2 100644 --- a/socket_basics/core/connector/normalizer.py +++ b/socket_basics/core/connector/normalizer.py @@ -46,6 +46,7 @@ def _normalize_alert(a: Dict[str, Any], connector: Any | None = None, default_ge if alert_matches_sast_ignore_override(a, override): logger.debug("Alert matched sast_ignore_overrides entry %s", override) a['action'] = 'ignore' + a['actionReason'] = 'sast_ignore_override' return a except Exception: logger.debug('Failed to check SAST ignore overrides for alert', exc_info=True) @@ -73,6 +74,7 @@ def _normalize_alert(a: Dict[str, Any], connector: Any | None = None, default_ge if rule_id in disabled_rules: logger.debug(f"Rule {rule_id} is disabled via {param}, setting action to 'ignore'") a['action'] = 'ignore' + a['actionReason'] = 'disabled_rule' return a except Exception: pass diff --git a/tests/test_sast_ignore_overrides.py b/tests/test_sast_ignore_overrides.py index 506fb1d..de36943 100644 --- a/tests/test_sast_ignore_overrides.py +++ b/tests/test_sast_ignore_overrides.py @@ -55,6 +55,7 @@ def test_normalize_alert_ignores_rule_only_override(): alert = _normalize_alert(_build_alert(), connector=connector) assert alert['action'] == 'ignore' + assert alert['actionReason'] == 'sast_ignore_override' def test_normalize_alert_ignores_matching_rule_and_path_override(): @@ -65,6 +66,7 @@ def test_normalize_alert_ignores_matching_rule_and_path_override(): alert = _normalize_alert(_build_alert(), connector=connector) assert alert['action'] == 'ignore' + assert alert['actionReason'] == 'sast_ignore_override' def test_normalize_alert_accepts_windows_style_override_paths(): @@ -75,6 +77,22 @@ def test_normalize_alert_accepts_windows_style_override_paths(): alert = _normalize_alert(_build_alert('src/unsafe/demo.js'), connector=connector) assert alert['action'] == 'ignore' + assert alert['actionReason'] == 'sast_ignore_override' + + +def test_get_sast_ignore_overrides_warns_when_path_does_not_exist(tmp_path, caplog): + config = Config( + { + 'workspace': str(tmp_path), + 'sast_ignore_overrides': 'js-sql-injection:src/services/credential_sync/api.ts', + } + ) + + parsed = config.get_sast_ignore_overrides() + + assert parsed == [{'rule_id': 'js-sql-injection', 'path': 'src/services/credential_sync/api.ts'}] + assert 'does not exist under workspace' in caplog.text + assert 'will not fall back to rule-only matching' in caplog.text def test_normalize_repo_relative_path_strips_github_actions_workspace_prefix(monkeypatch): @@ -126,6 +144,18 @@ def test_normalize_alert_does_not_ignore_non_matching_path_override(): alert = _normalize_alert(_build_alert(), connector=connector) assert alert['action'] == 'error' + assert 'actionReason' not in alert + + +def test_normalize_alert_marks_disabled_rule_ignores(): + connector = _DummyConnector( + Config({'workspace': '.', 'javascript_disabled_rules': 'js-sql-injection'}) + ) + + alert = _normalize_alert(_build_alert('src/services/credential_sync/api.ts'), connector=connector) + + assert alert['action'] == 'ignore' + assert alert['actionReason'] == 'disabled_rule' def test_count_blocking_alerts_skips_ignored_findings(): From 8648e0ba41b4cdb18e18c7a0cf2d4c895126fba7 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:59:35 -0400 Subject: [PATCH 15/17] chore: point action manifest at Dockerfile for branch testing Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/action.yml b/action.yml index b109084..fcdc9c6 100644 --- a/action.yml +++ b/action.yml @@ -4,7 +4,8 @@ author: "Socket" runs: using: "docker" - image: "docker://ghcr.io/socketdev/socket-basics:2.0.2" + # TODO: revert to the published GHCR image before merging/releasing. + image: "Dockerfile" env: # Core GitHub variables (these are automatically available, but we explicitly pass GITHUB_TOKEN) GITHUB_TOKEN: ${{ inputs.github_token }} From 248c8a74fb59b9e21d778aac260fffc7b90fe062 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 3 Apr 2026 01:21:17 -0400 Subject: [PATCH 16/17] fix: scope all-clear PR message to the correct scanner comment Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- .../core/notification/github_pr_notifier.py | 61 +++++++++++++- tests/test_github_pr_notifier.py | 79 ++++++++++++++++++- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/socket_basics/core/notification/github_pr_notifier.py b/socket_basics/core/notification/github_pr_notifier.py index 87f988c..7a32a74 100644 --- a/socket_basics/core/notification/github_pr_notifier.py +++ b/socket_basics/core/notification/github_pr_notifier.py @@ -54,12 +54,18 @@ def notify(self, facts: Dict[str, Any]) -> None: else: logger.warning('GithubPRNotifier: skipping invalid notification item: %s', type(item)) + notification_section_types = self._extract_section_types_from_notifications(valid_notifications) + facts_section_types = self._infer_section_types_from_facts(facts) + if not valid_notifications: if labels_enabled: pr_number = self._get_pr_number() if pr_number: self._reconcile_pr_labels(pr_number, []) - self._replace_existing_sections_with_all_clear(pr_number) + self._replace_existing_sections_with_all_clear( + pr_number, + section_types=facts_section_types, + ) else: logger.warning('GithubPRNotifier: unable to determine PR number for label reconciliation') logger.info('GithubPRNotifier: no notifications present; skipping comments') @@ -121,6 +127,10 @@ def notify(self, facts: Dict[str, Any]) -> None: else: logger.error('GithubPRNotifier: failed to post individual comment') + stale_section_types = facts_section_types - notification_section_types + if stale_section_types: + self._replace_existing_sections_with_all_clear(pr_number, section_types=stale_section_types) + # Add labels to PR if enabled if labels_enabled and pr_number: labels = self._determine_pr_labels(valid_notifications) @@ -296,6 +306,51 @@ def _extract_all_section_types(self, comment_body: str) -> List[str]: pattern = r'' return re.findall(pattern, comment_body or '') + def _extract_section_types_from_notifications(self, notifications: List[Dict[str, Any]]) -> set[str]: + """Return the managed section types present in notifier payload content.""" + section_types: set[str] = set() + for notification in notifications or []: + if not isinstance(notification, dict): + continue + section_match = self._extract_section_markers(notification.get('content', '')) + if section_match and section_match.get('type'): + section_types.add(section_match['type']) + return section_types + + def _infer_section_types_from_facts(self, facts: Dict[str, Any]) -> set[str]: + """Infer managed PR section types represented by the current run's components.""" + section_types: set[str] = set() + + for component in facts.get('components', []) or []: + if not isinstance(component, dict): + continue + + alerts = component.get('alerts', []) or [] + for alert in alerts: + if not isinstance(alert, dict): + continue + + generated_by = (alert.get('generatedBy') or '').strip().lower() + subtype = (alert.get('subType') or alert.get('subtype') or '').strip().lower() + + if subtype.startswith('sast-'): + section_types.add(subtype) + continue + + if generated_by == 'socket-tier1' or subtype == 'socket-tier1': + section_types.add('socket-tier1') + continue + + if generated_by == 'trufflehog' or subtype == 'secrets': + section_types.add('trufflehog-secrets') + continue + + if generated_by.startswith('trivy-') or subtype in {'dockerfile', 'container-image'}: + section_types.add('trivy-container') + continue + + return section_types + def _extract_section_title(self, section_content: str) -> str: """Extract the display title from a wrapped PR comment section.""" import re @@ -341,7 +396,7 @@ def _build_all_clear_section(self, section_type: str, existing_section_content: body = "✅ Socket Basics found no active findings in the latest run." return helpers.wrap_pr_comment_section(section_type, title, body, self.full_scan_url) - def _replace_existing_sections_with_all_clear(self, pr_number: int) -> None: + def _replace_existing_sections_with_all_clear(self, pr_number: int, section_types: Optional[set[str]] = None) -> None: """Rewrite existing managed PR comment sections to an all-clear state.""" existing_comments = self._get_pr_comments(pr_number) for comment in existing_comments: @@ -352,6 +407,8 @@ def _replace_existing_sections_with_all_clear(self, pr_number: int) -> None: updated_body = original_body changed = False for section_type in self._extract_all_section_types(original_body): + if section_types is not None and section_type not in section_types: + continue section_match = self._extract_section_markers(updated_body) if not section_match or section_match.get('type') != section_type: import re diff --git a/tests/test_github_pr_notifier.py b/tests/test_github_pr_notifier.py index 255795f..dac48d0 100644 --- a/tests/test_github_pr_notifier.py +++ b/tests/test_github_pr_notifier.py @@ -131,8 +131,85 @@ def test_notify_rewrites_existing_section_to_all_clear_when_notifications_are_em lambda pr_number, comment_id, body: updated_bodies.append(body) or True, ) - notifier.notify({'notifications': []}) + notifier.notify( + { + 'notifications': [], + 'components': [ + { + 'alerts': [ + { + 'generatedBy': 'opengrep-javascript', + 'subType': 'sast-javascript', + 'action': 'ignore', + } + ] + } + ], + } + ) assert len(updated_bodies) == 1 assert 'Socket Basics found no active findings in the latest run.' in updated_bodies[0] assert '' in updated_bodies[0] + + +def test_notify_empty_sast_notifications_do_not_rewrite_unrelated_sections(monkeypatch): + notifier = GithubPRNotifier( + { + 'repository': 'SocketDev/socket-basics', + 'pr_labels_enabled': True, + } + ) + + sast_comment = """ +## Socket SAST JavaScript + +### Summary +🟡 Medium: 1 +""" + tier1_comment = """ +## Socket Security Tier 1 + +### Summary +🟠 High: 2 +""" + updated_comments: list[tuple[int, str]] = [] + + monkeypatch.setattr(notifier, '_get_pr_number', lambda: 123) + monkeypatch.setattr(notifier, '_reconcile_pr_labels', lambda pr_number, labels: True) + monkeypatch.setattr( + notifier, + '_get_pr_comments', + lambda pr_number: [ + {'id': 99, 'body': sast_comment}, + {'id': 100, 'body': tier1_comment}, + ], + ) + monkeypatch.setattr( + notifier, + '_update_comment', + lambda pr_number, comment_id, body: updated_comments.append((comment_id, body)) or True, + ) + + notifier.notify( + { + 'notifications': [], + 'components': [ + { + 'alerts': [ + { + 'generatedBy': 'opengrep-javascript', + 'subType': 'sast-javascript', + 'action': 'ignore', + } + ] + } + ], + } + ) + + assert len(updated_comments) == 1 + assert updated_comments[0][0] == 99 + assert '' in updated_comments[0][1] + assert 'Socket Basics found no active findings in the latest run.' in updated_comments[0][1] + assert '' not in updated_comments[0][1] From 52accba642b8a60bbb997c8bd85e9e917574209d Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Fri, 3 Apr 2026 01:39:54 -0400 Subject: [PATCH 17/17] chore: restore action manifest to GHCR image path Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- action.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/action.yml b/action.yml index fcdc9c6..b109084 100644 --- a/action.yml +++ b/action.yml @@ -4,8 +4,7 @@ author: "Socket" runs: using: "docker" - # TODO: revert to the published GHCR image before merging/releasing. - image: "Dockerfile" + image: "docker://ghcr.io/socketdev/socket-basics:2.0.2" env: # Core GitHub variables (these are automatically available, but we explicitly pass GITHUB_TOKEN) GITHUB_TOKEN: ${{ inputs.github_token }}