ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_no_build_credentials.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Reject build-time ingestion or embedding of bench network credentials."""
5
6from __future__ import annotations
7
8import argparse
9import re
10import subprocess
11import sys
12import tempfile
13from pathlib import Path, PurePosixPath
14
15sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "dev"))
16
17from git_environment import isolated_git_environment, trusted_git_executable
18
19CMAKE_TOKENS = (
20 "RA8_C6_WIFI_SSID",
21 "RA8_C6_WIFI_PSK",
22 "RA8_MEDIA_DOWNLOAD_URL",
23 "openbao_client.py",
24 "wifi.env",
25)
26SOURCE_TOKENS = (
27 "RA8_C6_WIFI_SSID",
28 "RA8_C6_WIFI_PSK",
29 "RA8_MEDIA_DOWNLOAD_URL",
30)
31SOURCE_SUFFIXES = frozenset((".c", ".cc", ".cpp", ".cxx", ".m", ".mm", ".h", ".hh", ".hpp", ".hxx"))
32LEGACY_SCRIPT_PATTERNS = (
33 re.compile(r"load_c6_wifi_env\.py"),
34 re.compile(r"(?:source|\.)[^\n]*wifi\.env"),
35 re.compile(
36 r"openbao_client\.py(?:(?:\\\r?\n)|[^\n])*\bget\b"
37 r"(?:(?:\\\r?\n)|[^\n])*\bbench-network\b"
38 ),
39)
40JUST_CREDENTIAL_PATTERNS = tuple(
41 re.compile(pattern)
42 for token in SOURCE_TOKENS
43 for pattern in (
44 rf"(?:^|[\s;]){re.escape(token)}\s*=",
45 rf"\$(?:\{{{re.escape(token)}(?:\}}|[:?+\-])|{re.escape(token)}\b)",
46 )
47)
48MIN_CMAKE_FILES = 200
49MIN_SOURCE_FILES = 400
50MIN_SCRIPT_FILES = 40
51MIN_JUST_FILES = 20
52MIN_SCOPED_PARTS = 2
53
54
55def _is_source(path: PurePosixPath) -> bool:
56 """Return whether a path is first-party app/example C-family source."""
57 return path.suffix.lower() in SOURCE_SUFFIXES and path.parts[:1] in (
58 ("apps",),
59 ("examples",),
60 )
61
62
63def _is_scoped_script(path: PurePosixPath) -> bool:
64 """Return whether a path is build/HIL automation covered by this gate."""
65 return (
66 len(path.parts) >= MIN_SCOPED_PARTS
67 and path.parts[0] == "scripts"
68 and path.parts[1] in {"builders", "hil"}
69 )
70
71
72def _is_just_entry(path: PurePosixPath) -> bool:
73 """Return whether a path is a repository Just command entry point."""
74 return path.name == "justfile" or (path.parts[:1] == ("just",) and path.suffix == ".just")
75
76
77def findings_for(path: PurePosixPath, text: str) -> list[str]:
78 """Return forbidden build-credential mechanisms found in one file."""
79 findings: list[str] = []
80 is_cmake = path.name == "CMakeLists.txt" or path.suffix == ".cmake"
81 if is_cmake:
82 findings.extend(
83 f"build configuration consumes {token}" for token in CMAKE_TOKENS if token in text
84 )
85 if _is_source(path):
86 findings.extend(
87 f"firmware source embeds {token}" for token in SOURCE_TOKENS if token in text
88 )
89 if _is_scoped_script(path) or _is_just_entry(path):
90 findings.extend(
91 "automation uses a legacy build-time credential path"
92 for pattern in LEGACY_SCRIPT_PATTERNS
93 if pattern.search(text)
94 )
95 if _is_just_entry(path):
96 findings.extend(
97 "Just build entry point expands or assigns a credential variable"
98 for pattern in JUST_CREDENTIAL_PATTERNS
99 if pattern.search(text)
100 )
101 return findings
102
103
104def tracked_files(root: Path) -> list[PurePosixPath]:
105 """Return present tracked and untracked files, excluding ignored artifacts."""
106 git = trusted_git_executable()
107 result = subprocess.run( # noqa: S603 - executable resolved from PATH above
108 [git, "ls-files", "--cached", "--others", "--exclude-standard"],
109 cwd=root,
110 check=True,
111 capture_output=True,
112 text=True,
113 )
114 return [PurePosixPath(line) for line in result.stdout.splitlines() if (root / line).is_file()]
115
116
117def inspect_tree(
118 root: Path,
119 minimums: tuple[int, int, int, int] = (
120 MIN_CMAKE_FILES,
121 MIN_SOURCE_FILES,
122 MIN_SCRIPT_FILES,
123 MIN_JUST_FILES,
124 ),
125) -> tuple[list[str], tuple[int, int, int, int]]:
126 """Return violations and scope counts after scanning one Git worktree."""
127 violations: list[str] = []
128 cmake_count = 0
129 source_count = 0
130 script_count = 0
131 just_count = 0
132 for path in tracked_files(root):
133 is_cmake = path.name == "CMakeLists.txt" or path.suffix == ".cmake"
134 is_source = _is_source(path)
135 is_script = _is_scoped_script(path)
136 is_just = _is_just_entry(path)
137 if not (is_cmake or is_source or is_script or is_just):
138 continue
139 cmake_count += int(is_cmake)
140 source_count += int(is_source)
141 script_count += int(is_script)
142 just_count += int(is_just)
143 try:
144 text = (root / path).read_text(encoding="utf-8")
145 except (OSError, UnicodeDecodeError) as exc:
146 violations.append(f"{path}: cannot inspect file: {exc}")
147 continue
148 violations.extend(f"{path}: {finding}" for finding in findings_for(path, text))
149 min_cmake, min_source, min_script, min_just = minimums
150 if cmake_count < min_cmake:
151 violations.append(
152 f"scan reached only {cmake_count} CMake files; expected at least {min_cmake}"
153 )
154 if source_count < min_source:
155 violations.append(
156 "scan reached only "
157 f"{source_count} app/example source files; expected at least {min_source}"
158 )
159 if script_count < min_script:
160 violations.append(
161 f"scan reached only {script_count} build/HIL scripts; expected at least {min_script}"
162 )
163 if just_count < min_just:
164 violations.append(
165 f"scan reached only {just_count} Just entry points; expected at least {min_just}"
166 )
167 return violations, (cmake_count, source_count, script_count, just_count)
168
169
170def check_tree(root: Path) -> int:
171 """Scan the live tree and enforce non-vacuity floors for every scope."""
172 violations, counts = inspect_tree(root)
173 if violations:
174 for violation in violations:
175 print(f"check_no_build_credentials.py: {violation}", file=sys.stderr)
176 return 1
177 cmake_count, source_count, script_count, just_count = counts
178 print(
179 "check_no_build_credentials.py: "
180 f"{cmake_count} CMake + {source_count} app/example source + "
181 f"{script_count} build/HIL script + {just_count} Just files are credential-free"
182 )
183 return 0
184
185
186def _write_selftest_file(root: Path, relative: str, text: str) -> None:
187 """Create one selftest fixture inside a temporary Git worktree."""
188 path = root / relative
189 path.parent.mkdir(parents=True, exist_ok=True)
190 path.write_text(text, encoding="utf-8")
191
192
193def _selftest_cases() -> tuple[tuple[PurePosixPath, str, bool], ...]:
194 """Return direct must-fire and must-stay-quiet detection cases."""
195 return (
196 (
197 PurePosixPath("examples/board/app/CMakeLists.txt"),
198 'target_compile_definitions(app PRIVATE RA8_C6_WIFI_PSK="secret")',
199 True,
200 ),
201 (
202 PurePosixPath("examples/board/app/src/main.c"),
203 "static const char psk[] = RA8_C6_WIFI_PSK;",
204 True,
205 ),
206 (
207 PurePosixPath("scripts/hil/run.sh"),
208 "python3 scripts/secrets/openbao_client.py \\\n"
209 " get secret/ra8d2/bench-network bench_psk",
210 True,
211 ),
212 (
213 PurePosixPath("examples/board/app/src/main.mm"),
214 "static const char psk[] = RA8_C6_WIFI_PSK;",
215 True,
216 ),
217 (
218 PurePosixPath("just/apps.just"),
219 'build:\n RA8_C6_WIFI_PSK="$RA8_C6_WIFI_PSK" cmake --build build',
220 True,
221 ),
222 (
223 PurePosixPath("examples/board/app/CMakeLists.txt"),
224 "target_sources(app PRIVATE src/main.c)",
225 False,
226 ),
227 (
228 PurePosixPath("examples/board/app/src/main.c"),
229 'puts("ra8_net_provision: READY v1");',
230 False,
231 ),
232 (
233 PurePosixPath("scripts/hil/run.sh"),
234 "python3 scripts/secrets/wifi_provision.py emit | ssh bench cat",
235 False,
236 ),
237 )
238
239
240def _case_selftest_failures() -> list[str]:
241 """Return failures from the direct detection cases."""
242 failures = []
243 for path, text, should_fire in _selftest_cases():
244 fired = bool(findings_for(path, text))
245 if fired != should_fire:
246 failures.append(f"{path}: expected fired={should_fire}, got {fired}")
247 return failures
248
249
250def _seed_selftest_worktree(root: Path, git: str) -> None:
251 """Create and track the clean end-to-end selftest fixture set."""
252 subprocess.run( # noqa: S603 -- resolved Git executable and fixed fixture argv
253 [git, "init", "-q"], cwd=root, check=True
254 )
255 _write_selftest_file(root, "CMakeLists.txt", "project(selftest C)\n")
256 _write_selftest_file(
257 root,
258 "examples/board/app/src/main.cpp",
259 "int main() { return 0; }\n",
260 )
261 _write_selftest_file(root, "scripts/hil/run.sh", "#!/usr/bin/env bash\ntrue\n")
262 _write_selftest_file(root, "just/apps.just", "build:\n true\n")
263 subprocess.run( # noqa: S603 -- resolved Git executable and fixed fixture argv
264 [git, "add", "."], cwd=root, check=True
265 )
266
267
268def _worktree_selftest_failures(git: str) -> list[str]:
269 """Return failures from tracked-file, untracked-file, and floor checks."""
270 failures = []
271 with tempfile.TemporaryDirectory() as name:
272 root = Path(name)
273 _seed_selftest_worktree(root, git)
274 clean_findings, clean_counts = inspect_tree(root, (1, 1, 1, 1))
275 if clean_findings or clean_counts != (1, 1, 1, 1):
276 failures.append("end-to-end clean worktree or scope counts failed")
277
278 _write_selftest_file(
279 root,
280 "examples/board/app/src/leak.hxx",
281 "#define RA8_C6_WIFI_PSK secret\n",
282 )
283 leak_findings, _ = inspect_tree(root, (1, 1, 1, 1))
284 if not any("leak.hxx" in finding for finding in leak_findings):
285 failures.append("end-to-end untracked C++ header leak was missed")
286
287 floor_findings, _ = inspect_tree(root, (2, 3, 2, 2))
288 floor_scopes = (
289 "CMake files",
290 "source files",
291 "build/HIL scripts",
292 "Just entry points",
293 )
294 if not all(any(scope in finding for finding in floor_findings) for scope in floor_scopes):
295 failures.append("end-to-end non-vacuity floors did not all fire")
296 return failures
297
298
299def _run_selftest_body() -> int:
300 """Prove legacy embedding fires while runtime provisioning stays quiet."""
301 failures = _case_selftest_failures()
302
303 git = trusted_git_executable()
304 failures.extend(_worktree_selftest_failures(git))
305 if failures:
306 for failure in failures:
307 print(f"check_no_build_credentials.py --selftest: FAIL: {failure}", file=sys.stderr)
308 return 1
309 print(
310 "check_no_build_credentials.py --selftest: PASS "
311 "(5 must-fire, 3 must-stay-quiet, tracked-file and floor cases)"
312 )
313 return 0
314
315
316def run_selftest() -> int:
317 """Run temporary-repository cases without inheriting the caller's repo."""
318 with isolated_git_environment():
319 return _run_selftest_body()
320
321
322def main() -> int:
323 """Parse arguments and run either the selftest or live scan."""
324 parser = argparse.ArgumentParser(description=__doc__)
325 parser.add_argument("--selftest", action="store_true")
326 args = parser.parse_args()
327 if args.selftest:
328 return run_selftest()
329 return check_tree(Path(__file__).resolve().parents[2])
330
331
332if __name__ == "__main__":
333 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298