4"""Reject build-time ingestion or embedding of bench network credentials."""
6from __future__
import annotations
13from pathlib
import Path, PurePosixPath
15sys.path.insert(0, str(Path(__file__).resolve().parents[1] /
"dev"))
17from git_environment
import isolated_git_environment, trusted_git_executable
22 "RA8_MEDIA_DOWNLOAD_URL",
29 "RA8_MEDIA_DOWNLOAD_URL",
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"),
36 r"openbao_client\.py(?:(?:\\\r?\n)|[^\n])*\bget\b"
37 r"(?:(?:\\\r?\n)|[^\n])*\bbench-network\b"
40JUST_CREDENTIAL_PATTERNS = tuple(
42 for token
in SOURCE_TOKENS
44 rf
"(?:^|[\s;]){re.escape(token)}\s*=",
45 rf
"\$(?:\{{{re.escape(token)}(?:\}}|[:?+\-])|{re.escape(token)}\b)",
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 (
63def _is_scoped_script(path: PurePosixPath) -> bool:
64 """Return whether a path is build/HIL automation covered by this gate."""
66 len(path.parts) >= MIN_SCOPED_PARTS
67 and path.parts[0] ==
"scripts"
68 and path.parts[1]
in {
"builders",
"hil"}
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")
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"
83 f
"build configuration consumes {token}" for token
in CMAKE_TOKENS
if token
in text
87 f
"firmware source embeds {token}" for token
in SOURCE_TOKENS
if token
in text
89 if _is_scoped_script(path)
or _is_just_entry(path):
91 "automation uses a legacy build-time credential path"
92 for pattern
in LEGACY_SCRIPT_PATTERNS
93 if pattern.search(text)
95 if _is_just_entry(path):
97 "Just build entry point expands or assigns a credential variable"
98 for pattern
in JUST_CREDENTIAL_PATTERNS
99 if pattern.search(text)
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(
108 [git,
"ls-files",
"--cached",
"--others",
"--exclude-standard"],
114 return [PurePosixPath(line)
for line
in result.stdout.splitlines()
if (root / line).is_file()]
119 minimums: tuple[int, int, int, int] = (
125) -> tuple[list[str], tuple[int, int, int, int]]:
126 """Return violations and scope counts after scanning one Git worktree."""
127 violations: list[str] = []
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):
139 cmake_count += int(is_cmake)
140 source_count += int(is_source)
141 script_count += int(is_script)
142 just_count += int(is_just)
144 text = (root / path).read_text(encoding=
"utf-8")
145 except (OSError, UnicodeDecodeError)
as exc:
146 violations.append(f
"{path}: cannot inspect file: {exc}")
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:
152 f
"scan reached only {cmake_count} CMake files; expected at least {min_cmake}"
154 if source_count < min_source:
157 f
"{source_count} app/example source files; expected at least {min_source}"
159 if script_count < min_script:
161 f
"scan reached only {script_count} build/HIL scripts; expected at least {min_script}"
163 if just_count < min_just:
165 f
"scan reached only {just_count} Just entry points; expected at least {min_just}"
167 return violations, (cmake_count, source_count, script_count, just_count)
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)
174 for violation
in violations:
175 print(f
"check_no_build_credentials.py: {violation}", file=sys.stderr)
177 cmake_count, source_count, script_count, just_count = counts
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"
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")
193def _selftest_cases() -> tuple[tuple[PurePosixPath, str, bool], ...]:
194 """Return direct must-fire and must-stay-quiet detection cases."""
197 PurePosixPath(
"examples/board/app/CMakeLists.txt"),
198 'target_compile_definitions(app PRIVATE RA8_C6_WIFI_PSK="secret")',
202 PurePosixPath(
"examples/board/app/src/main.c"),
203 "static const char psk[] = RA8_C6_WIFI_PSK;",
207 PurePosixPath(
"scripts/hil/run.sh"),
208 "python3 scripts/secrets/openbao_client.py \\\n"
209 " get secret/ra8d2/bench-network bench_psk",
213 PurePosixPath(
"examples/board/app/src/main.mm"),
214 "static const char psk[] = RA8_C6_WIFI_PSK;",
218 PurePosixPath(
"just/apps.just"),
219 'build:\n RA8_C6_WIFI_PSK="$RA8_C6_WIFI_PSK" cmake --build build',
223 PurePosixPath(
"examples/board/app/CMakeLists.txt"),
224 "target_sources(app PRIVATE src/main.c)",
228 PurePosixPath(
"examples/board/app/src/main.c"),
229 'puts("ra8_net_provision: READY v1");',
233 PurePosixPath(
"scripts/hil/run.sh"),
234 "python3 scripts/secrets/wifi_provision.py emit | ssh bench cat",
240def _case_selftest_failures() -> list[str]:
241 """Return failures from the direct detection cases."""
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}")
250def _seed_selftest_worktree(root: Path, git: str) ->
None:
251 """Create and track the clean end-to-end selftest fixture set."""
253 [git,
"init",
"-q"], cwd=root, check=
True
255 _write_selftest_file(root,
"CMakeLists.txt",
"project(selftest C)\n")
256 _write_selftest_file(
258 "examples/board/app/src/main.cpp",
259 "int main() { return 0; }\n",
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")
264 [git,
"add",
"."], cwd=root, check=
True
268def _worktree_selftest_failures(git: str) -> list[str]:
269 """Return failures from tracked-file, untracked-file, and floor checks."""
271 with tempfile.TemporaryDirectory()
as 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")
278 _write_selftest_file(
280 "examples/board/app/src/leak.hxx",
281 "#define RA8_C6_WIFI_PSK secret\n",
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")
287 floor_findings, _ = inspect_tree(root, (2, 3, 2, 2))
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")
299def _run_selftest_body() -> int:
300 """Prove legacy embedding fires while runtime provisioning stays quiet."""
301 failures = _case_selftest_failures()
303 git = trusted_git_executable()
304 failures.extend(_worktree_selftest_failures(git))
306 for failure
in failures:
307 print(f
"check_no_build_credentials.py --selftest: FAIL: {failure}", file=sys.stderr)
310 "check_no_build_credentials.py --selftest: PASS "
311 "(5 must-fire, 3 must-stay-quiet, tracked-file and floor cases)"
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()
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()
328 return run_selftest()
329 return check_tree(Path(__file__).resolve().parents[2])
332if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.