Skip to content

Commit f68ab29

Browse files
committed
fix: support local SAST ignore overrides by rule id, path
Signed-off-by: lelia <2418071+lelia@users.noreply.github.com>
1 parent 5aa26ce commit f68ab29

7 files changed

Lines changed: 274 additions & 2 deletions

File tree

action.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ author: "Socket"
44

55
runs:
66
using: "docker"
7-
image: "docker://ghcr.io/socketdev/socket-basics:2.0.2"
7+
# TODO: revert to the published GHCR image before merging/releasing.
8+
image: "Dockerfile"
89
env:
910
# Core GitHub variables (these are automatically available, but we explicitly pass GITHUB_TOKEN)
1011
GITHUB_TOKEN: ${{ inputs.github_token }}
@@ -40,6 +41,7 @@ runs:
4041
INPUT_JAVASCRIPT_DISABLED_RULES: ${{ inputs.javascript_disabled_rules }}
4142
INPUT_JAVASCRIPT_ENABLED_RULES: ${{ inputs.javascript_enabled_rules }}
4243
INPUT_JAVASCRIPT_SAST_ENABLED: ${{ inputs.javascript_sast_enabled }}
44+
INPUT_SAST_IGNORE_OVERRIDES: ${{ inputs.sast_ignore_overrides }}
4345
INPUT_JAVA_DISABLED_RULES: ${{ inputs.java_disabled_rules }}
4446
INPUT_JAVA_ENABLED_RULES: ${{ inputs.java_enabled_rules }}
4547
INPUT_JAVA_SAST_ENABLED: ${{ inputs.java_sast_enabled }}
@@ -246,6 +248,10 @@ inputs:
246248
description: "Enable JavaScript/TypeScript SAST scanning"
247249
required: false
248250
default: "false"
251+
sast_ignore_overrides:
252+
description: "Comma-separated list of SAST ignore overrides in rule_id or rule_id:path format"
253+
required: false
254+
default: ""
249255
jira_api_token:
250256
description: "Jira Api Token"
251257
required: false
@@ -453,4 +459,4 @@ inputs:
453459

454460
branding:
455461
icon: "shield"
456-
color: "blue"
462+
color: "purple"

docs/github-action.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,8 +638,25 @@ jobs:
638638
# JavaScript with custom rules
639639
javascript_sast_enabled: 'true'
640640
javascript_enabled_rules: 'eval-usage,prototype-pollution'
641+
642+
# Ignore a specific SAST rule globally or for one exact file
643+
sast_ignore_overrides: 'js-sql-injection:index.js'
641644
```
642645

646+
`sast_ignore_overrides` supports:
647+
- `rule_id` to ignore a SAST rule everywhere in the repo
648+
- `rule_id:path` to ignore a SAST rule for one exact repo-relative file
649+
650+
Examples:
651+
- `js-sql-injection`
652+
- `js-sql-injection:index.js`
653+
- `js-sql-injection:src/unsafe/demo.js`
654+
655+
Notes:
656+
- Paths must be exact repo-relative paths using `/` separators after normalization.
657+
- Windows-style input such as `src\\unsafe\\demo.js` is accepted and normalized automatically.
658+
- Globs and directory-prefix matching are not supported in this first version.
659+
643660
## Configuration Reference
644661

645662
### All Available Inputs
@@ -667,6 +684,7 @@ See [`action.yml`](../action.yml) for the complete list of inputs.
667684
**Rule Configuration (per language):**
668685
- `<language>_enabled_rules` — Comma-separated rules to enable
669686
- `<language>_disabled_rules` — Comma-separated rules to disable
687+
- `sast_ignore_overrides` — Comma-separated `rule_id` or `rule_id:path` SAST ignore overrides
670688

671689
**Security Scanning:**
672690
- `secret_scanning_enabled` — Enable secret scanning

docs/parameters.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,25 @@ socket-basics --go --go-enabled-rules "error-handling,sql-injection"
241241
- `--rust-enabled-rules` / `--rust-disabled-rules`
242242
- `--elixir-enabled-rules` / `--elixir-disabled-rules`
243243

244+
### `--sast-ignore-overrides SAST_IGNORE_OVERRIDES`
245+
Comma-separated list of SAST ignore overrides in `rule_id` or `rule_id:path` format.
246+
247+
**Environment Variable:** `INPUT_SAST_IGNORE_OVERRIDES`
248+
249+
**Examples:**
250+
```bash
251+
# Ignore a rule everywhere in the repo
252+
socket-basics --javascript --sast-ignore-overrides "js-sql-injection"
253+
254+
# Ignore a rule only for one exact repo-relative file
255+
socket-basics --javascript --sast-ignore-overrides "js-sql-injection:index.js"
256+
```
257+
258+
Notes:
259+
- Paths must be exact repo-relative paths.
260+
- Paths are normalized to forward-slash form, so Windows-style input such as `src\\unsafe\\demo.js` is accepted.
261+
- Globs and directory-prefix matching are not supported in this first version.
262+
244263
### `--opengrep-notify OPENGREP_NOTIFY`
245264
Notification method for OpenGrep SAST results (e.g., console, slack).
246265

@@ -520,6 +539,7 @@ All notification integrations support environment variables as alternatives to C
520539
| Variable | Description |
521540
|----------|-------------|
522541
| `INPUT_OPENGREP_RULES_DIR` | Custom directory containing SAST rules |
542+
| `INPUT_SAST_IGNORE_OVERRIDES` | Comma-separated `rule_id` or `rule_id:path` SAST ignore overrides |
523543

524544
## Configuration File
525545

@@ -537,6 +557,7 @@ You can provide configuration via a JSON file using `--config`:
537557
"python_sast_enabled": true,
538558
"javascript_sast_enabled": true,
539559
"go_sast_enabled": true,
560+
"sast_ignore_overrides": "js-sql-injection:index.js",
540561

541562
"secrets_enabled": true,
542563
"trufflehog_exclude_dir": "node_modules,vendor,dist,.git",

socket_basics/connectors.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ connectors:
191191
env_variable: INPUT_JAVASCRIPT_DISABLED_RULES
192192
type: str
193193
default: ""
194+
- name: sast_ignore_overrides
195+
option: --sast-ignore-overrides
196+
description: "Comma-separated list of SAST ignore overrides in rule_id or rule_id:path format"
197+
env_variable: INPUT_SAST_IGNORE_OVERRIDES
198+
type: str
199+
default: ""
194200

195201
# Go rule configuration
196202
- name: go_enabled_rules

socket_basics/core/config.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,107 @@
1515
logger = logging.getLogger(__name__)
1616

1717

18+
def normalize_repo_relative_path(path_value: str | None) -> str | None:
19+
"""Normalize a repo-relative path to the POSIX form emitted by SAST alerts."""
20+
if path_value is None:
21+
return None
22+
23+
path_str = str(path_value).strip()
24+
if not path_str:
25+
return None
26+
27+
# Accept common local input styles but keep the final format strict.
28+
path_str = path_str.replace('\\', '/')
29+
while path_str.startswith('./'):
30+
path_str = path_str[2:]
31+
path_str = path_str.lstrip('/')
32+
33+
normalized_parts: List[str] = []
34+
for part in path_str.split('/'):
35+
if not part or part == '.':
36+
continue
37+
if part == '..':
38+
return None
39+
normalized_parts.append(part)
40+
41+
normalized = '/'.join(normalized_parts)
42+
return normalized or None
43+
44+
45+
def parse_sast_ignore_overrides(raw_value: str | None) -> List[Dict[str, str | None]]:
46+
"""Parse `rule_id` and `rule_id:path` ignore override entries."""
47+
overrides: List[Dict[str, str | None]] = []
48+
seen: set[tuple[str, str | None]] = set()
49+
50+
if not raw_value:
51+
return overrides
52+
53+
for raw_entry in str(raw_value).split(','):
54+
entry = raw_entry.strip()
55+
if not entry:
56+
continue
57+
58+
rule_id = entry
59+
path = None
60+
61+
if ':' in entry:
62+
rule_id, path_part = entry.split(':', 1)
63+
rule_id = rule_id.strip()
64+
path_part = path_part.strip()
65+
66+
if not rule_id or not path_part:
67+
logger.warning("Ignoring malformed SAST ignore override: %r", entry)
68+
continue
69+
70+
if any(ch in path_part for ch in ('*', '?', '[')):
71+
logger.warning(
72+
"Ignoring unsupported SAST ignore override with glob syntax: %r",
73+
entry,
74+
)
75+
continue
76+
77+
path = normalize_repo_relative_path(path_part)
78+
if not path:
79+
logger.warning("Ignoring invalid repo-relative path in SAST override: %r", entry)
80+
continue
81+
else:
82+
rule_id = rule_id.strip()
83+
if not rule_id:
84+
logger.warning("Ignoring malformed SAST ignore override: %r", entry)
85+
continue
86+
87+
key = (rule_id, path)
88+
if key in seen:
89+
continue
90+
seen.add(key)
91+
overrides.append({'rule_id': rule_id, 'path': path})
92+
93+
return overrides
94+
95+
96+
def alert_matches_sast_ignore_override(
97+
alert: Dict[str, Any],
98+
override: Dict[str, str | None],
99+
) -> bool:
100+
"""Return True when an alert matches a parsed SAST ignore override."""
101+
props = alert.get('props', {}) or {}
102+
rule_id = props.get('ruleId') or alert.get('title') or alert.get('ruleId')
103+
if not rule_id or rule_id != override.get('rule_id'):
104+
return False
105+
106+
override_path = override.get('path')
107+
if not override_path:
108+
return True
109+
110+
alert_path = (
111+
props.get('filePath')
112+
or (alert.get('location') or {}).get('path')
113+
or ''
114+
)
115+
normalized_alert_path = normalize_repo_relative_path(alert_path)
116+
return normalized_alert_path == override_path
117+
118+
18119
class Config:
19120
"""Configuration object that provides unified access to all settings"""
20121

@@ -112,6 +213,13 @@ def get_action_for_severity(self, severity: str) -> str:
112213
else:
113214
# Default action for unknown severities
114215
return 'monitor'
216+
217+
def get_sast_ignore_overrides(self) -> List[Dict[str, str | None]]:
218+
"""Return parsed SAST ignore overrides from config."""
219+
if not hasattr(self, '_sast_ignore_overrides_cache'):
220+
raw_value = self.get('sast_ignore_overrides', '')
221+
self._sast_ignore_overrides_cache = parse_sast_ignore_overrides(raw_value)
222+
return self._sast_ignore_overrides_cache
115223

116224
@property
117225
def repo(self) -> str:

socket_basics/core/connector/opengrep/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,9 @@ def generate_notifications(self, components: List[Dict[str, Any]]) -> Dict[str,
720720
groups: Dict[str, List[Dict[str, Any]]] = {}
721721
for c in comps_map.values():
722722
for a in c.get('alerts', []):
723+
alert_action = (a.get('action') or '').strip().lower()
724+
if alert_action == 'ignore':
725+
continue
723726
# Filter by severity - only include alerts that match allowed severities
724727
alert_severity = (a.get('severity') or '').strip().lower()
725728
if alert_severity and hasattr(self, 'allowed_severities') and alert_severity not in self.allowed_severities:
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from socket_basics.core.config import (
2+
Config,
3+
parse_sast_ignore_overrides,
4+
)
5+
from socket_basics.core.connector.normalizer import _normalize_alert
6+
from socket_basics.core.connector.opengrep import OpenGrepScanner
7+
from socket_basics.socket_basics import count_blocking_alerts
8+
9+
10+
class _DummyConnector:
11+
def __init__(self, config: Config):
12+
self.config = config
13+
14+
15+
def _build_alert(path: str = 'index.js') -> dict:
16+
return {
17+
'title': 'js-sql-injection',
18+
'severity': 'critical',
19+
'props': {
20+
'ruleId': 'js-sql-injection',
21+
'filePath': path,
22+
'startLine': 14,
23+
'endLine': 14,
24+
},
25+
'location': {
26+
'path': path,
27+
'start': 14,
28+
'end': 14,
29+
},
30+
}
31+
32+
33+
def test_parse_sast_ignore_overrides_supports_rule_and_exact_path():
34+
parsed = parse_sast_ignore_overrides(
35+
'js-sql-injection, js-sql-injection:./src/db/query.js'
36+
)
37+
38+
assert parsed == [
39+
{'rule_id': 'js-sql-injection', 'path': None},
40+
{'rule_id': 'js-sql-injection', 'path': 'src/db/query.js'},
41+
]
42+
43+
44+
def test_parse_sast_ignore_overrides_skips_glob_paths(caplog):
45+
parsed = parse_sast_ignore_overrides('js-sql-injection:src/**/*.js')
46+
47+
assert parsed == []
48+
assert 'glob syntax' in caplog.text
49+
50+
51+
def test_normalize_alert_ignores_rule_only_override():
52+
connector = _DummyConnector(Config({'workspace': '.', 'sast_ignore_overrides': 'js-sql-injection'}))
53+
54+
alert = _normalize_alert(_build_alert(), connector=connector)
55+
56+
assert alert['action'] == 'ignore'
57+
58+
59+
def test_normalize_alert_ignores_matching_rule_and_path_override():
60+
connector = _DummyConnector(
61+
Config({'workspace': '.', 'sast_ignore_overrides': 'js-sql-injection:index.js'})
62+
)
63+
64+
alert = _normalize_alert(_build_alert(), connector=connector)
65+
66+
assert alert['action'] == 'ignore'
67+
68+
69+
def test_normalize_alert_accepts_windows_style_override_paths():
70+
connector = _DummyConnector(
71+
Config({'workspace': '.', 'sast_ignore_overrides': r'js-sql-injection:src\unsafe\demo.js'})
72+
)
73+
74+
alert = _normalize_alert(_build_alert('src/unsafe/demo.js'), connector=connector)
75+
76+
assert alert['action'] == 'ignore'
77+
78+
79+
def test_normalize_alert_does_not_ignore_non_matching_path_override():
80+
connector = _DummyConnector(
81+
Config({'workspace': '.', 'sast_ignore_overrides': 'js-sql-injection:src/index.js'})
82+
)
83+
84+
alert = _normalize_alert(_build_alert(), connector=connector)
85+
86+
assert alert['action'] == 'error'
87+
88+
89+
def test_count_blocking_alerts_skips_ignored_findings():
90+
results = {
91+
'components': [
92+
{'id': 'ignored.js', 'alerts': [{**_build_alert('ignored.js'), 'action': 'ignore'}]},
93+
{'id': 'active.js', 'alerts': [{**_build_alert('active.js'), 'action': 'error'}]},
94+
]
95+
}
96+
97+
assert count_blocking_alerts(results) == 1
98+
99+
100+
def test_opengrep_notifications_skip_ignored_findings():
101+
scanner = OpenGrepScanner(Config({'workspace': '.'}))
102+
component = {
103+
'id': 'index.js',
104+
'qualifiers': {'scanner': 'opengrep', 'type': 'javascript'},
105+
'alerts': [{**_build_alert(), 'action': 'ignore', 'subType': 'sast-javascript'}],
106+
}
107+
108+
notifications = scanner.generate_notifications([component])
109+
110+
assert notifications == {}

0 commit comments

Comments
 (0)