Skip to content

Commit 400a2fa

Browse files
committed
feat: add inline ignore support
1 parent 6a23233 commit 400a2fa

7 files changed

Lines changed: 610 additions & 7 deletions

File tree

.sentinelscanignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,5 @@ venv/
44
# Ignore Python cache directories
55
__pycache__/
66

7-
# Ignore fixture directory
8-
test_dirs/
9-
107
# Ignore minified Python files
118
*.min.py

inline_ignore.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import io
2+
import tokenize
3+
4+
5+
INLINE_IGNORE_MARKER = "sentinelscan: ignore"
6+
7+
8+
def line_has_inline_ignore(line):
9+
"""
10+
Return True when a source line contains a SentinelScan inline ignore comment.
11+
12+
Only comment tokens are checked, so string literals containing the ignore
13+
marker do not suppress findings.
14+
"""
15+
try:
16+
tokens = tokenize.generate_tokens(io.StringIO(line).readline)
17+
except tokenize.TokenError:
18+
return False
19+
20+
for token in tokens:
21+
token_type = token.type
22+
token_value = token.string
23+
24+
if token_type == tokenize.COMMENT and INLINE_IGNORE_MARKER in token_value:
25+
return True
26+
27+
return False
28+
29+

scanner.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +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
67

78
def check_path(input_path):
89
"""
@@ -61,10 +62,14 @@ def scan(files):
6162
for file in files:
6263
with open(file, "r", encoding="utf-8", errors="ignore") as f:
6364
content = f.read()
65+
lines = content.splitlines()
6466

6567
ast_results = detect_ast_secrets(content)
6668

6769
for finding in ast_results:
70+
line = lines[finding.line_number - 1]
71+
if line_has_inline_ignore(line):
72+
continue
6873
finding_with_file = replace(finding, file_path=str(file))
6974
findings.append(finding_with_file)
7075

test_dirs/test_repo/open_vulns.py

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

tests/test_cli.py

Lines changed: 290 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1117,4 +1117,293 @@ def test_cli_invalid_choice_fails(args, expected_error):
11171117
def test_cli_invalid_path_prints_error():
11181118
result = run_cli("does_not_exist")
11191119

1120-
assert "[ERROR]" in result.stdout or "[ERROR]" in result.stderr
1120+
assert "[ERROR]" in result.stdout or "[ERROR]" in result.stderr
1121+
1122+
def test_cli_json_inline_ignore_suppresses_finding(tmp_path):
1123+
write_python_file(
1124+
tmp_path,
1125+
"ignored.py",
1126+
'password = "abcdef" # sentinelscan: ignore\n',
1127+
)
1128+
1129+
result = run_cli(tmp_path, "--json")
1130+
assert_success(result)
1131+
1132+
data = parse_json_output(result)
1133+
1134+
assert data == []
1135+
1136+
1137+
def test_cli_json_inline_ignore_keeps_non_ignored_findings(tmp_path):
1138+
findings_file = write_python_file(
1139+
tmp_path,
1140+
"findings.py",
1141+
'password = "abcdef" # sentinelscan: ignore\n'
1142+
'token = "abc1234567890j"\n',
1143+
)
1144+
1145+
result = run_cli(tmp_path, "--json")
1146+
assert_success(result)
1147+
1148+
data = parse_json_output(result)
1149+
1150+
assert_single_json_finding(
1151+
data,
1152+
line=2,
1153+
file=findings_file,
1154+
var_name="token",
1155+
rule_id="TOKEN",
1156+
rule="Token",
1157+
severity="MEDIUM",
1158+
value="abc1234567890j",
1159+
reason=TOKEN_REASON,
1160+
confidence="HIGH",
1161+
)
1162+
1163+
1164+
def test_cli_json_unrelated_comment_does_not_suppress_finding(tmp_path):
1165+
findings_file = write_python_file(
1166+
tmp_path,
1167+
"findings.py",
1168+
'password = "abcdef" # normal comment\n',
1169+
)
1170+
1171+
result = run_cli(tmp_path, "--json")
1172+
assert_success(result)
1173+
1174+
data = parse_json_output(result)
1175+
1176+
assert_single_json_finding(
1177+
data,
1178+
line=1,
1179+
file=findings_file,
1180+
var_name="password",
1181+
rule_id="PASSWORD",
1182+
rule="Password",
1183+
severity="HIGH",
1184+
value="abcdef",
1185+
reason=PASSWORD_REASON,
1186+
confidence="LOW",
1187+
)
1188+
1189+
1190+
def test_cli_json_inline_ignore_suppresses_multiple_findings_on_same_line(tmp_path):
1191+
write_python_file(
1192+
tmp_path,
1193+
"findings.py",
1194+
'api_key = "AKIAEXAMPLE123456789" # sentinelscan: ignore\n',
1195+
)
1196+
1197+
result = run_cli(tmp_path, "--json")
1198+
assert_success(result)
1199+
1200+
data = parse_json_output(result)
1201+
1202+
assert data == []
1203+
1204+
1205+
def test_cli_json_inline_ignore_works_with_severity_filter(tmp_path):
1206+
findings_file = write_python_file(
1207+
tmp_path,
1208+
"findings.py",
1209+
'password = "abcdef" # sentinelscan: ignore\n'
1210+
'random_var = "AKIAEXAMPLE123456789"\n',
1211+
)
1212+
1213+
result = run_cli(tmp_path, "--json", "--severity", "HIGH")
1214+
assert_success(result)
1215+
1216+
data = parse_json_output(result)
1217+
1218+
assert_single_json_finding(
1219+
data,
1220+
line=2,
1221+
file=findings_file,
1222+
var_name="random_var",
1223+
rule_id="AWS_ACCESS_KEY",
1224+
rule="AWS Access Key",
1225+
severity="HIGH",
1226+
value="AKIAEXAMPLE123456789",
1227+
reason="value matched AKIA-prefixed AWS access key pattern",
1228+
confidence="HIGH",
1229+
)
1230+
1231+
1232+
def test_cli_json_inline_ignore_works_with_confidence_filter(tmp_path):
1233+
findings_file = write_python_file(
1234+
tmp_path,
1235+
"findings.py",
1236+
'password = "abcdef" # sentinelscan: ignore\n'
1237+
'token = "abc1234567890j"\n',
1238+
)
1239+
1240+
result = run_cli(tmp_path, "--json", "--confidence", "HIGH")
1241+
assert_success(result)
1242+
1243+
data = parse_json_output(result)
1244+
1245+
assert_single_json_finding(
1246+
data,
1247+
line=2,
1248+
file=findings_file,
1249+
var_name="token",
1250+
rule_id="TOKEN",
1251+
rule="Token",
1252+
severity="MEDIUM",
1253+
value="abc1234567890j",
1254+
reason=TOKEN_REASON,
1255+
confidence="HIGH",
1256+
)
1257+
1258+
1259+
def test_cli_json_inline_ignore_works_with_redaction(tmp_path):
1260+
findings_file = write_python_file(
1261+
tmp_path,
1262+
"findings.py",
1263+
'password = "abcdef" # sentinelscan: ignore\n'
1264+
'token = "abc1234567890j"\n',
1265+
)
1266+
1267+
result = run_cli(tmp_path, "--json", "--redact")
1268+
assert_success(result)
1269+
1270+
data = parse_json_output(result)
1271+
1272+
assert_single_json_finding(
1273+
data,
1274+
line=2,
1275+
file=findings_file,
1276+
var_name="token",
1277+
rule_id="TOKEN",
1278+
rule="Token",
1279+
severity="MEDIUM",
1280+
value="ab**********0j",
1281+
reason=TOKEN_REASON,
1282+
confidence="HIGH",
1283+
)
1284+
1285+
1286+
def test_cli_json_inline_ignore_and_sentinelscanignore_work_together(tmp_path):
1287+
findings_file = write_python_file(
1288+
tmp_path,
1289+
"src/findings.py",
1290+
'password = "abcdef" # sentinelscan: ignore\n'
1291+
'token = "abc1234567890j"\n',
1292+
)
1293+
write_python_file(
1294+
tmp_path,
1295+
"ignored.py",
1296+
'random_var = "AKIAEXAMPLE123456789"\n',
1297+
)
1298+
1299+
ignore_file = tmp_path / ".sentinelscanignore"
1300+
ignore_file.write_text("ignored.py\n", encoding="utf-8")
1301+
1302+
result = run_cli(tmp_path, "--json")
1303+
assert_success(result)
1304+
1305+
data = parse_json_output(result)
1306+
1307+
assert_single_json_finding(
1308+
data,
1309+
line=2,
1310+
file=findings_file,
1311+
var_name="token",
1312+
rule_id="TOKEN",
1313+
rule="Token",
1314+
severity="MEDIUM",
1315+
value="abc1234567890j",
1316+
reason=TOKEN_REASON,
1317+
confidence="HIGH",
1318+
)
1319+
1320+
1321+
# -------------------------
1322+
# CLI text inline ignore behavior
1323+
# -------------------------
1324+
1325+
1326+
def test_cli_text_inline_ignore_suppresses_finding(tmp_path):
1327+
write_python_file(
1328+
tmp_path,
1329+
"ignored.py",
1330+
'password = "abcdef" # sentinelscan: ignore\n',
1331+
)
1332+
1333+
result = run_cli(tmp_path)
1334+
assert_success(result)
1335+
1336+
assert "No vulnerabilities found." in result.stdout
1337+
assert "abcdef" not in result.stdout
1338+
assert "Reason:" not in result.stdout
1339+
assert "Confidence:" not in result.stdout
1340+
1341+
1342+
def test_cli_text_inline_ignore_keeps_non_ignored_findings(tmp_path):
1343+
write_python_file(
1344+
tmp_path,
1345+
"findings.py",
1346+
'password = "abcdef" # sentinelscan: ignore\n'
1347+
'token = "abc1234567890j"\n',
1348+
)
1349+
1350+
result = run_cli(tmp_path)
1351+
assert_success(result)
1352+
1353+
assert "password" not in result.stdout
1354+
assert "abcdef" not in result.stdout
1355+
assert "Token" in result.stdout
1356+
assert "abc1234567890j" in result.stdout
1357+
assert "Reason:" in result.stdout
1358+
assert "Confidence:" in result.stdout
1359+
1360+
1361+
def test_cli_text_unrelated_comment_does_not_suppress_finding(tmp_path):
1362+
write_python_file(
1363+
tmp_path,
1364+
"findings.py",
1365+
'password = "abcdef" # normal comment\n',
1366+
)
1367+
1368+
result = run_cli(tmp_path)
1369+
assert_success(result)
1370+
1371+
assert "[HIGH]" in result.stdout
1372+
assert "Password" in result.stdout
1373+
assert "abcdef" in result.stdout
1374+
assert "Reason:" in result.stdout
1375+
assert "Confidence:" in result.stdout
1376+
1377+
1378+
def test_cli_text_inline_ignore_suppresses_multiple_findings_on_same_line(tmp_path):
1379+
write_python_file(
1380+
tmp_path,
1381+
"findings.py",
1382+
'api_key = "AKIAEXAMPLE123456789" # sentinelscan: ignore\n',
1383+
)
1384+
1385+
result = run_cli(tmp_path)
1386+
assert_success(result)
1387+
1388+
assert "No vulnerabilities found." in result.stdout
1389+
assert "AWS Access Key" not in result.stdout
1390+
assert "API Key" not in result.stdout
1391+
assert "AKIAEXAMPLE123456789" not in result.stdout
1392+
1393+
1394+
def test_cli_text_inline_ignore_works_with_redaction(tmp_path):
1395+
write_python_file(
1396+
tmp_path,
1397+
"findings.py",
1398+
'password = "abcdef" # sentinelscan: ignore\n'
1399+
'token = "abc1234567890j"\n',
1400+
)
1401+
1402+
result = run_cli(tmp_path, "--redact")
1403+
assert_success(result)
1404+
1405+
assert "abcdef" not in result.stdout
1406+
assert "a****f" not in result.stdout
1407+
assert "abc1234567890j" not in result.stdout
1408+
assert "ab**********0j" in result.stdout
1409+
assert "Token" in result.stdout

tests/test_ignore.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from pathlib import Path
2-
31
from ignore import (
42
filter_ignored_files,
53
find_ignore_file,

0 commit comments

Comments
 (0)