Option C Tools
Loading...
Searching...
No Matches
test_no_hardcoded_secrets.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_linter/test_no_hardcoded_secrets.py
4
5"""
6Purpose
7-------
8Validate the linter's ``no_hardcoded_secrets`` rule (§6b Secret Hygiene).
9
10Responsibilities
11----------------
12- Confirm that hardcoded secrets in direct assignments are caught (Pattern A).
13- Confirm that hardcoded secrets in annotated assignments are caught (Pattern A).
14- Confirm that hardcoded secrets in dict literals are caught (Pattern B).
15- Confirm that hardcoded secrets in function defaults are caught (Pattern C).
16- Confirm that safe patterns (os.getenv, os.environ, function calls,
17 subscripts) pass without violations.
18- Confirm that non-secret variable names with string literals pass.
19- Confirm profile behaviour (strict/compact: blocking; proto: not checked).
20
21Diagnostics
22-----------
23Domain: LINTER-TESTS
24L2: test lifecycle
25L3: assertion details
26L4: deep tracing
27
28Contracts
29---------
30Inputs: inline source strings
31Outputs: pass/fail assertions
32"""
33
34from oct.linter import oct_lint
35
36
37# =====================================================================
38# Pattern A — Direct assignments
39# =====================================================================
40
42 """String literal assigned to a secret-like variable name is caught."""
43 source = 'password = "hunter2"\n'
44 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
45 assert not ok
46 assert "password" in msg
47 assert "string literal" in msg
48
49
51 """Multiple secret assignments produce multiple violation messages."""
52 source = (
53 'API_KEY = "sk-abc123"\n'
54 'token = "tok-xyz"\n'
55 )
56 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
57 assert not ok
58 assert "API_KEY" in msg or "api_key" in msg.lower()
59 assert "token" in msg
60
61
63 """Annotated assignment (name: str = "literal") is caught."""
64 source = 'password: str = "hunter2"\n'
65 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
66 assert not ok
67 assert "password" in msg
68
69
71 """Class-level secret attribute is caught (AST walker visits class bodies)."""
72 source = (
73 'class Config:\n'
74 ' API_KEY = "sk-abc"\n'
75 )
76 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
77 assert not ok
78 assert "API_KEY" in msg or "api_key" in msg.lower()
79
80
81# =====================================================================
82# Pattern B — Dict literals
83# =====================================================================
84
86 """Dict with secret-like key mapped to a string literal is caught."""
87 source = 'config = {"password": "secret"}\n'
88 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
89 assert not ok
90 assert "password" in msg
91
92
94 """Dict with multiple secret keys produces multiple violations."""
95 source = 'settings = {"api_key": "sk-123", "db_url": "postgres://x"}\n'
96 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
97 assert not ok
98 assert "api_key" in msg
99 assert "db_url" in msg
100
101
103 """Dict with non-secret key and string value passes."""
104 source = 'config = {"max_retries": "3"}\n'
105 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
106 assert ok
107
108
110 """Dict with secret key mapped to a function call passes."""
111 source = (
112 'import os\n'
113 'config = {"password": os.getenv("DB_PASS")}\n'
114 )
115 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
116 assert ok
117
118
119# =====================================================================
120# Pattern C — Function default arguments
121# =====================================================================
122
124 """Function with secret-like parameter defaulting to string literal is caught."""
125 source = 'def connect(password="default"):\n pass\n'
126 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
127 assert not ok
128 assert "password" in msg
129
130
132 """Keyword-only parameter with secret default is caught."""
133 source = 'def connect(*, token="default"):\n pass\n'
134 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
135 assert not ok
136 assert "token" in msg
137
138
140 """Function with non-secret parameter defaulting to string passes."""
141 source = 'def connect(host="localhost"):\n pass\n'
142 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
143 assert ok
144
145
146# =====================================================================
147# Safe patterns — should all pass
148# =====================================================================
149
151 """os.getenv() is a function call, not a literal — passes."""
152 source = (
153 'import os\n'
154 'password = os.getenv("PASSWORD")\n'
155 )
156 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
157 assert ok
158
159
161 """os.environ["KEY"] is a subscript, not a literal — passes."""
162 source = (
163 'import os\n'
164 'API_KEY = os.environ["API_KEY"]\n'
165 )
166 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
167 assert ok
168
169
171 """Assigning a function call result to a secret name passes."""
172 source = 'password = get_password()\n'
173 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
174 assert ok
175
176
178 """Subscript on a non-environ object passes (known limitation)."""
179 source = (
180 'config = load_config("settings.json")\n'
181 'API_KEY = config["api_key"]\n'
182 )
183 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
184 assert ok
185
186
188 """Variable with non-secret name assigned string literal passes."""
189 source = 'max_retries = "3"\n'
190 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
191 assert ok
192
193
195 """f-string assigned to secret-named variable is flagged (OI-408)."""
196 source = 'password = f"prefix-{var}"\n'
197 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
198 assert not ok
199 assert "f-string" in msg
200
201
203 """String concatenation assigned to secret-named variable is flagged (OI-408)."""
204 source = 'api_key = "sk-" + "abc123"\n'
205 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
206 assert not ok
207 assert "concatenation" in msg
208
209
211 """Concatenation where one side is a literal is flagged (OI-408)."""
212 source = 'token = prefix + "static_suffix"\n'
213 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
214 assert not ok
215 assert "concatenation" in msg
216
217
219 """f-string assigned to a non-secret variable name is fine (OI-408)."""
220 source = 'greeting = f"Hello {name}"\n'
221 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
222 assert ok
223
224
226 """Empty file passes."""
227 ok, msg = oct_lint.check_no_hardcoded_secrets("")
228 assert ok
229
230
232 """Unparseable source passes (same behaviour as other AST-based checks)."""
233 ok, msg = oct_lint.check_no_hardcoded_secrets("def (\n")
234 assert ok
235
236
237# =====================================================================
238# Comprehensive coverage of all secret name patterns
239# =====================================================================
240
242 """Every name in _SECRET_NAME_PATTERNS is caught when assigned a literal."""
243 for name in oct_lint._SECRET_NAME_PATTERNS:
244 source = f'{name} = "test_value"\n'
245 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
246 assert not ok, f"Expected violation for '{name}' but check passed"
247
248
250 """Secret name matching is case-insensitive."""
251 source = 'PASSWORD = "hunter2"\n'
252 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
253 assert not ok
254 assert "PASSWORD" in msg
255
256
257# =====================================================================
258# OI-402 — Compound / substring name matching
259# =====================================================================
260
262 """Compound variable names containing secret patterns are caught (OI-402)."""
263 source = 'db_password = "hunter2"\n'
264 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
265 assert not ok
266 assert "db_password" in msg
267
268
270 """Prefixed secret name like stripe_api_key is caught (OI-402)."""
271 source = 'stripe_api_key = "sk_live_abc123"\n'
272 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
273 assert not ok
274
275
277 """Suffixed secret name like refresh_token_value is caught (OI-402)."""
278 source = 'refresh_token_value = "tok-xyz"\n'
279 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
280 assert not ok
281
282
284 """Compound name that does not contain a secret pattern passes."""
285 source = 'max_retry_count = "3"\n'
286 ok, msg = oct_lint.check_no_hardcoded_secrets(source)
287 assert ok
288
289
290# =====================================================================
291# OI-404 — Waiver reason enforcement
292# =====================================================================
293
295 """parse_inline_waivers sets reason=None when no reason is given (OI-404)."""
296 source = '# OCT-LINT: disable=no_hardcoded_secrets\npassword = "hunter2"\n'
297 waivers = oct_lint.parse_inline_waivers(source)
298 assert len(waivers) == 1
299 w = list(waivers.values())[0]
300 assert w["reason"] is None
301
302
304 """parse_inline_waivers captures reason when provided (OI-404)."""
305 source = '# OCT-LINT: disable=no_hardcoded_secrets reason="test fixture"\n'
306 waivers = oct_lint.parse_inline_waivers(source)
307 w = list(waivers.values())[0]
308 assert w["reason"] is not None
309 assert "test fixture" in w["reason"]
310
311
313 """The waived_rules set only includes waivers with a reason (OI-404)."""
314 source = (
315 '# OCT-LINT: disable=rule_a\n'
316 '# OCT-LINT: disable=rule_b reason="justified"\n'
317 )
318 waivers = oct_lint.parse_inline_waivers(source)
319 waived_rules = {w["rule"] for w in waivers.values() if w["reason"] is not None}
320 assert "rule_a" not in waived_rules, "reason-less waiver should not suppress check"
321 assert "rule_b" in waived_rules, "waiver with reason should suppress check"