Skip to content

Commit ab94588

Browse files
committed
feat: add subscript usage detection
1 parent f932895 commit ab94588

7 files changed

Lines changed: 451 additions & 196 deletions

File tree

detectors/ast_analyzer.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def extract_variable_path(node):
8484
temp_node = temp_node.value
8585

8686
# Ignore unsupported roots such as function calls or subscripts.
87-
if not isinstance(temp_node, ast.Name):
87+
if not isinstance(temp_node, ast.Name) or isinstance(temp_node, ast.Subscript):
8888
continue
8989

9090
full_path.append(temp_node.id.lower())
@@ -93,7 +93,15 @@ def extract_variable_path(node):
9393
# Handle direct variable assignment.
9494
elif isinstance(var, ast.Name):
9595
full_path.append(var.id.lower())
96+
97+
elif isinstance(var, ast.Subscript):
98+
key = var.slice
9699

100+
if not (isinstance(key, ast.Constant) and isinstance(key.value, str)):
101+
continue
102+
103+
full_path.append(key.value.lower())
104+
97105
if full_path:
98106
yield full_path
99107

test_dirs/test_repo/open_vulns.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,3 @@
66
api_key = "12dwdqwdqwdqw3"
77
token = "xyzgggggg" # noqa: E702
88
TOKEN = "abc1234567890j"
9-

tests/test_ast/__init__.py

Whitespace-only changes.

tests/test_ast/ast_helpers.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from detectors.find_secrets import detect_ast_secrets # noqa: F401
2+
3+
PASSWORD_REASON = (
4+
"variable name matched password/pwd/passwd pattern and value met minimum length"
5+
)
6+
API_KEY_REASON = (
7+
"variable name matched api_key/apikey pattern and value met minimum length"
8+
)
9+
TOKEN_REASON = "variable name matched token pattern and value met minimum length"
10+
SECRET_REASON = "variable name matched secret pattern and value met minimum length"
11+
AWS_REASON = "value matched AKIA-prefixed AWS access key pattern"
12+
13+
SUPPORTED_CONFIDENCE = {"LOW", "MEDIUM", "HIGH"}
14+
15+
16+
def get_entropy(finding):
17+
"""
18+
Return entropy metadata from a Finding.
19+
20+
Supports either `entropy` or `entropy_score` if the field name changes
21+
during refactoring.
22+
"""
23+
if hasattr(finding, "entropy"):
24+
return finding.entropy
25+
26+
if hasattr(finding, "entropy_score"):
27+
return finding.entropy_score
28+
29+
raise AssertionError("Finding is missing entropy metadata")
30+
31+
32+
def assert_entropy_metadata(finding):
33+
"""
34+
Assert that entropy metadata exists and is numeric.
35+
"""
36+
entropy = get_entropy(finding)
37+
38+
assert isinstance(entropy, (int, float))
39+
assert entropy >= 0
40+
41+
42+
def assert_finding(
43+
finding,
44+
*,
45+
line_number,
46+
file_path=None,
47+
var_name,
48+
value,
49+
rule_id,
50+
rule_name,
51+
severity,
52+
reason,
53+
confidence=None,
54+
):
55+
"""
56+
Assert stable Finding fields while allowing future metadata fields.
57+
"""
58+
assert finding.file_path == file_path
59+
assert finding.line_number == line_number
60+
assert finding.var_name == var_name
61+
assert finding.value == value
62+
assert finding.rule_id == rule_id
63+
assert finding.rule_name == rule_name
64+
assert finding.severity == severity
65+
assert finding.reason == reason
66+
67+
if confidence is None:
68+
assert finding.confidence in SUPPORTED_CONFIDENCE
69+
else:
70+
assert finding.confidence == confidence
71+
72+
assert_entropy_metadata(finding)
73+
74+
75+
def assert_single_finding(result, **expected):
76+
"""
77+
Assert that detection produced exactly one expected finding.
78+
"""
79+
assert len(result) == 1
80+
assert_finding(result[0], **expected)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from tests.test_ast.ast_helpers import (
2+
API_KEY_REASON,
3+
PASSWORD_REASON,
4+
TOKEN_REASON,
5+
assert_single_finding,
6+
detect_ast_secrets,
7+
)
8+
9+
10+
def test_ast_attribute_password():
11+
code = 'self.password = "abcdef"'
12+
result = detect_ast_secrets(code)
13+
14+
assert_single_finding(
15+
result,
16+
line_number=1,
17+
var_name="password",
18+
value="abcdef",
19+
rule_id="PASSWORD",
20+
rule_name="Password",
21+
severity="HIGH",
22+
reason=PASSWORD_REASON,
23+
confidence="LOW",
24+
)
25+
26+
27+
def test_ast_attribute_api_key():
28+
code = 'config.api_key = "12345678"'
29+
result = detect_ast_secrets(code)
30+
31+
assert_single_finding(
32+
result,
33+
line_number=1,
34+
var_name="api_key",
35+
value="12345678",
36+
rule_id="API_KEY",
37+
rule_name="API Key",
38+
severity="HIGH",
39+
reason=API_KEY_REASON,
40+
)
41+
42+
43+
def test_ast_attribute_token():
44+
code = 'user.token = "qwerty123"'
45+
result = detect_ast_secrets(code)
46+
47+
assert_single_finding(
48+
result,
49+
line_number=1,
50+
var_name="token",
51+
value="qwerty123",
52+
rule_id="TOKEN",
53+
rule_name="Token",
54+
severity="MEDIUM",
55+
reason=TOKEN_REASON,
56+
)
57+
58+
59+
def test_ast_nested_attribute_password():
60+
code = 'self.config.db.password = "abcdef"'
61+
result = detect_ast_secrets(code)
62+
63+
assert_single_finding(
64+
result,
65+
line_number=1,
66+
var_name="password",
67+
value="abcdef",
68+
rule_id="PASSWORD",
69+
rule_name="Password",
70+
severity="HIGH",
71+
reason=PASSWORD_REASON,
72+
confidence="LOW",
73+
)
74+
75+
76+
def test_ast_deep_nested_attribute_api_key():
77+
code = 'settings.auth.credentials.api_key = "12345678"'
78+
result = detect_ast_secrets(code)
79+
80+
assert_single_finding(
81+
result,
82+
line_number=1,
83+
var_name="api_key",
84+
value="12345678",
85+
rule_id="API_KEY",
86+
rule_name="API Key",
87+
severity="HIGH",
88+
reason=API_KEY_REASON,
89+
)

0 commit comments

Comments
 (0)