Skip to content

Commit f932895

Browse files
committed
feat: implement rule specific inline ignore
1 parent 54f51e2 commit f932895

5 files changed

Lines changed: 371 additions & 113 deletions

File tree

inline_ignore.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
INLINE_IGNORE_MARKER = "sentinelscan: ignore"
66

77

8-
def line_has_inline_ignore(line):
8+
def finding_has_inline_ignore(line, finding):
99
"""
1010
Return True when a source line contains a SentinelScan inline ignore comment.
1111
12-
Only comment tokens are checked, so string literals containing the ignore
13-
marker do not suppress findings.
12+
Generic ignores suppress all findings on the line.
13+
Rule-specific ignores suppress only listed rule IDs.
1414
"""
1515
try:
1616
tokens = tokenize.generate_tokens(io.StringIO(line).readline)
@@ -21,7 +21,19 @@ def line_has_inline_ignore(line):
2121
token_type = token.type
2222
token_value = token.string
2323

24-
if token_type == tokenize.COMMENT and INLINE_IGNORE_MARKER in token_value:
24+
if token_type != tokenize.COMMENT:
25+
continue
26+
27+
if INLINE_IGNORE_MARKER not in token_value:
28+
continue
29+
30+
after_ignore = token_value.partition(INLINE_IGNORE_MARKER)[2].strip()
31+
ignored_rules = after_ignore.split()
32+
33+
if not ignored_rules:
34+
return True
35+
36+
if finding.rule_id in ignored_rules:
2537
return True
2638

2739
return False

scanner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from detectors.find_secrets import detect_ast_secrets
55
from ignore import filter_ignored_files, load_ignore_patterns
6-
from inline_ignore import line_has_inline_ignore
6+
from inline_ignore import finding_has_inline_ignore
77

88
def check_path(input_path):
99
"""
@@ -68,7 +68,7 @@ def scan(files):
6868

6969
for finding in ast_results:
7070
line = lines[finding.line_number - 1]
71-
if line_has_inline_ignore(line):
71+
if finding_has_inline_ignore(line, finding):
7272
continue
7373
finding_with_file = replace(finding, file_path=str(file))
7474
findings.append(finding_with_file)

test_dirs/test_repo/open_vulns.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
api_key = "123"
22
password = "abc"
33
token = "xyz"
4-
aws_key = "AKIAEXAMPLE123456789" # sentinelscan: ignore
4+
aws_key = "AKIAEXAMPLE123456789" # sentinelscan: ignore AWS_ACCESS_KE
55
token = "xyzttttggfdddf"
66
api_key = "12dwdqwdqwdqw3"
77
token = "xyzgggggg" # noqa: E702

tests/test_cli/test_cli_inline_ignore.py

Lines changed: 181 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,4 +291,184 @@ def test_cli_text_inline_ignore_works_with_redaction(tmp_path):
291291
assert "a****f" not in result.stdout
292292
assert "abc1234567890j" not in result.stdout
293293
assert "ab**********0j" in result.stdout
294-
assert "Token" in result.stdout
294+
assert "Token" in result.stdout
295+
296+
297+
def test_cli_json_rule_specific_ignore_suppresses_only_matching_rule(tmp_path):
298+
findings_file = write_python_file(
299+
tmp_path,
300+
"findings.py",
301+
'api_key = "AKIAEXAMPLE123456789" # sentinelscan: ignore AWS_ACCESS_KEY\n',
302+
)
303+
304+
result = run_cli(tmp_path, "--json")
305+
assert_success(result)
306+
307+
data = parse_json_output(result)
308+
309+
assert_single_json_finding(
310+
data,
311+
line=1,
312+
file=findings_file,
313+
var_name="api_key",
314+
rule_id="API_KEY",
315+
rule="API Key",
316+
severity="HIGH",
317+
value="AKIAEXAMPLE123456789",
318+
reason="variable name matched api_key/apikey pattern and value met minimum length",
319+
confidence="HIGH",
320+
)
321+
322+
323+
def test_cli_json_rule_specific_ignore_can_keep_aws_match(tmp_path):
324+
findings_file = write_python_file(
325+
tmp_path,
326+
"findings.py",
327+
'api_key = "AKIAEXAMPLE123456789" # sentinelscan: ignore API_KEY\n',
328+
)
329+
330+
result = run_cli(tmp_path, "--json")
331+
assert_success(result)
332+
333+
data = parse_json_output(result)
334+
335+
assert_single_json_finding(
336+
data,
337+
line=1,
338+
file=findings_file,
339+
var_name="api_key",
340+
rule_id="AWS_ACCESS_KEY",
341+
rule="AWS Access Key",
342+
severity="HIGH",
343+
value="AKIAEXAMPLE123456789",
344+
reason=AWS_REASON,
345+
confidence="HIGH",
346+
)
347+
348+
349+
def test_cli_json_rule_specific_ignore_suppresses_multiple_listed_rules(tmp_path):
350+
write_python_file(
351+
tmp_path,
352+
"findings.py",
353+
'api_key = "AKIAEXAMPLE123456789" '
354+
'# sentinelscan: ignore AWS_ACCESS_KEY API_KEY\n',
355+
)
356+
357+
result = run_cli(tmp_path, "--json")
358+
assert_success(result)
359+
360+
data = parse_json_output(result)
361+
362+
assert data == []
363+
364+
365+
def test_cli_json_unknown_rule_specific_ignore_does_not_suppress(tmp_path):
366+
findings_file = write_python_file(
367+
tmp_path,
368+
"findings.py",
369+
'password = "abcdef" # sentinelscan: ignore FAKE_RULE\n',
370+
)
371+
372+
result = run_cli(tmp_path, "--json")
373+
assert_success(result)
374+
375+
data = parse_json_output(result)
376+
377+
assert_single_json_finding(
378+
data,
379+
line=1,
380+
file=findings_file,
381+
var_name="password",
382+
rule_id="PASSWORD",
383+
rule="Password",
384+
severity="HIGH",
385+
value="abcdef",
386+
reason=PASSWORD_REASON,
387+
confidence="LOW",
388+
)
389+
390+
391+
def test_cli_text_rule_specific_ignore_suppresses_only_matching_rule(tmp_path):
392+
write_python_file(
393+
tmp_path,
394+
"findings.py",
395+
'api_key = "AKIAEXAMPLE123456789" # sentinelscan: ignore AWS_ACCESS_KEY\n',
396+
)
397+
398+
result = run_cli(tmp_path)
399+
assert_success(result)
400+
401+
assert "API Key" in result.stdout
402+
assert "AWS Access Key" not in result.stdout
403+
assert "AKIAEXAMPLE123456789" in result.stdout
404+
assert "Confidence:" in result.stdout
405+
assert "Reason:" in result.stdout
406+
407+
408+
def test_cli_text_rule_specific_ignore_can_keep_aws_match(tmp_path):
409+
write_python_file(
410+
tmp_path,
411+
"findings.py",
412+
'api_key = "AKIAEXAMPLE123456789" # sentinelscan: ignore API_KEY\n',
413+
)
414+
415+
result = run_cli(tmp_path)
416+
assert_success(result)
417+
418+
assert "AWS Access Key" in result.stdout
419+
assert "API Key" not in result.stdout
420+
assert "AKIAEXAMPLE123456789" in result.stdout
421+
assert "Confidence:" in result.stdout
422+
assert "Reason:" in result.stdout
423+
424+
425+
def test_cli_text_rule_specific_ignore_suppresses_multiple_listed_rules(tmp_path):
426+
write_python_file(
427+
tmp_path,
428+
"findings.py",
429+
'api_key = "AKIAEXAMPLE123456789" '
430+
'# sentinelscan: ignore AWS_ACCESS_KEY API_KEY\n',
431+
)
432+
433+
result = run_cli(tmp_path)
434+
assert_success(result)
435+
436+
assert "No vulnerabilities found." in result.stdout
437+
assert "AWS Access Key" not in result.stdout
438+
assert "API Key" not in result.stdout
439+
assert "AKIAEXAMPLE123456789" not in result.stdout
440+
441+
442+
def test_cli_json_rule_specific_ignore_works_with_filters_and_redaction(tmp_path):
443+
findings_file = write_python_file(
444+
tmp_path,
445+
"findings.py",
446+
'password = "abcdef" # sentinelscan: ignore PASSWORD\n'
447+
'token = "abc1234567890j"\n',
448+
)
449+
450+
result = run_cli(
451+
tmp_path,
452+
"--json",
453+
"--severity",
454+
"MEDIUM",
455+
"--confidence",
456+
"HIGH",
457+
"--redact",
458+
)
459+
assert_success(result)
460+
461+
data = parse_json_output(result)
462+
463+
assert_single_json_finding(
464+
data,
465+
line=2,
466+
file=findings_file,
467+
var_name="token",
468+
rule_id="TOKEN",
469+
rule="Token",
470+
severity="MEDIUM",
471+
value="ab**********0j",
472+
reason=TOKEN_REASON,
473+
confidence="HIGH",
474+
)

0 commit comments

Comments
 (0)