3"""Structural caller validation for protected and sourced-only shell files.
5The typed entry-point table owns which shell files require privileged startup
6and which files may only be sourced. This module owns how executable and
7configuration text may refer to those files. It deliberately parses argv
8structure instead of looking for a few weak Bash spellings.
11from __future__
import annotations
17from collections.abc
import Iterable, Mapping
18from dataclasses
import dataclass
20import shell_invocation_selftest_cases
as fixtures
21from shell_entrypoint_policy
import (
27from shell_invocation_references
import (
32 SHELL_FENCE_LANGUAGES,
36SHELL_CONTROL = frozenset({
";",
"&&",
"||",
"|",
"&",
"(",
")"})
37SHELL_PREFIX = frozenset({
"if",
"elif",
"while",
"until",
"then",
"do",
"!",
"{"})
38DECLARATION_PREFIX = frozenset({
"declare",
"export",
"local",
"readonly",
"typeset"})
39MARKDOWN_FENCE = re.compile(
r"^\s*(```+|~~~+)\s*([A-Za-z0-9_+-]*)\s*$")
40PRIVILEGED_TARGET_INDEX = 2
41JUST_RECIPE = re.compile(
r"^[A-Za-z_][A-Za-z0-9_-]*(?:\s+[^:]*)?:\s*$")
42NONCOMMAND_SHELL_PREFIX = frozenset({
"[[",
"case",
"for",
"function",
"select"})
43RELEASE_LOADER_NAME =
"source_release_selftest_helper_from"
44RELEASE_LOADER_MAIN_REL =
"scripts/dev/provision_dev_box_toolchain.sh"
45RELEASE_LOADER_MAIN_LOGICAL = (
46 'source_release_selftest_helper_from "$main" "$helper" '
47 '"$expected_dir" "$expected_digest" || return 1'
49RELEASE_LOADER_FIXTURE_REL =
"scripts/dev/provision_dev_box_toolchain_selftest.bash"
50RELEASE_LOADER_FIXTURE_LOGICAL = (
51 'source_release_selftest_helper_from "$main" "$helper" "$directory" "$digest" || status=$?'
53RELEASE_LOADER_GRAMMARS = {
54 RELEASE_LOADER_MAIN_REL: (RELEASE_LOADER_MAIN_LOGICAL, ShellUsage.ENTRY,
True,
False),
55 RELEASE_LOADER_FIXTURE_REL: (
56 RELEASE_LOADER_FIXTURE_LOGICAL,
57 ShellUsage.SOURCED_ONLY,
64@dataclass(frozen=True)
66 """One structural caller-policy violation."""
72@dataclass(frozen=True)
73class ShellSegmentContext:
74 """Immutable caller and source context for one logical shell command."""
76 variables: Mapping[str, str]
77 policies: Mapping[str, ShellPolicy]
78 caller_policy: ShellPolicy |
None
85def guarded_paths(policies: Mapping[str, ShellPolicy]) -> frozenset[str]:
86 """Return all privileged or source-only paths requiring caller review."""
89 for path, policy
in policies.items()
90 if policy.security
is ShellSecurity.PRIVILEGED
or policy.usage
is ShellUsage.SOURCED_ONLY
94def _target_for_token(token: str, policies: Mapping[str, ShellPolicy]) -> str |
None:
95 """Resolve an exact or rooted token to one governed repository path."""
96 normalized = token.replace(
"\\",
"/")
97 if normalized.startswith(
"file://"):
98 normalized = normalized.removeprefix(
"file://")
99 for target
in guarded_paths(policies):
100 if normalized == target
or normalized.endswith(f
"/{target}"):
105def _expand_token(token: str, variables: Mapping[str, str]) -> str:
106 """Resolve the deliberately small scalar shell-dataflow vocabulary."""
107 if token.startswith(
"${")
and token.endswith(
"}"):
108 return variables.get(token[2:-1], token)
109 if token.startswith(
"$")
and token[1:].isidentifier():
110 return variables.get(token[1:], token)
114def _normalize_shell(logical: str) -> str:
115 """Return stable whitespace for exact reviewed shell references."""
116 return " ".join(logical.split())
119def _is_exact_reference(rel: str |
None, logical: str, target: str |
None =
None) -> bool:
120 """Return whether an exact governed occurrence has a written non-launch reason."""
123 normalized = _normalize_shell(logical)
126 and entry.logical == normalized
127 and (target
is None or entry.target == target)
128 for entry
in EXACT_REFERENCES
134 policies: Mapping[str, ShellPolicy],
135 caller_policy: ShellPolicy |
None,
137 """Validate every governed target in one resolved argument vector."""
138 targets = [(_target_for_token(token, policies), index)
for index, token
in enumerate(argv)]
139 governed = [(target, index)
for target, index
in targets
if target
is not None]
140 findings: list[str] = []
141 for target, index
in governed:
144 policy = policies[target]
145 source_call = bool(argv)
and argv[0]
in {
".",
"source"}
and index == 1
147 if policy.usage
not in {ShellUsage.SOURCED_ONLY, ShellUsage.DUAL_USE}:
148 findings.append(f
"entry-only target is sourced: {target}")
149 elif policy.source_requires_privileged_parent
and (
150 caller_policy
is None or caller_policy.security
is not ShellSecurity.PRIVILEGED
152 findings.append(f
"privileged source has no privileged parent: {target}")
156 and policy.executable
164 index == PRIVILEGED_TARGET_INDEX
165 and argv[:2] == [
"/bin/bash",
"-p"]
166 and policy.security
is ShellSecurity.PRIVILEGED
167 and policy.usage
is not ShellUsage.SOURCED_ONLY
169 if not direct
and not privileged_argv:
171 "governed target lacks direct verified shebang or exact "
172 f
"/bin/bash -p argv: {target}"
177def _logical_shell_lines(text: str) -> list[tuple[int, str]]:
178 """Join shell continuations and omit quoted heredoc payloads."""
179 logicals: list[tuple[int, str]] = []
180 lines = text.splitlines()
182 heredoc_end: str |
None =
None
183 while index < len(lines):
187 if heredoc_end
is not None:
188 if line.strip() == heredoc_end:
192 while index < len(lines)
and (
193 logical.rstrip().endswith(
"\\")
or logical.rstrip().endswith((
"|",
"||",
"&&"))
195 stripped = logical.rstrip()
196 logical = (stripped.removesuffix(
"\\")) + lines[index]
198 heredoc = re.search(
r"<<-?\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\1", logical)
199 if heredoc
is not None:
200 heredoc_end = heredoc.group(2)
201 logicals.append((number, logical))
205def _shell_tokens(logical: str) -> list[str] |
None:
206 """Decode one shell logical line, preserving command separators."""
207 lexer = shlex.shlex(logical, posix=
True, punctuation_chars=
";&|()")
208 lexer.commenters =
"#"
209 lexer.whitespace_split =
True
216def _command_segments(tokens: list[str]) -> Iterable[list[str]]:
217 """Yield argv-like shell command segments."""
218 segment: list[str] = []
220 if token
in SHELL_CONTROL:
225 segment.append(token)
230def _env_command_index(arguments: list[str]) -> int:
231 """Return the nested executable index for fixed env-style argv."""
233 while index < len(arguments):
234 argument = arguments[index]
235 if argument
in {
"-u",
"--unset"}:
238 if argument.startswith(
"-")
or re.match(
r"^[A-Za-z_][A-Za-z0-9_]*=", argument):
245def _xargs_command_index(arguments: list[str]) -> int:
246 """Return the nested executable index for the supported xargs option grammar."""
248 options_with_value = {
"-a",
"-E",
"-I",
"-L",
"-n",
"-P",
"-s"}
249 while index < len(arguments):
250 argument = arguments[index]
251 if argument ==
"-" * 2:
253 if argument
in options_with_value:
255 elif argument.startswith((
"-I",
"-E",
"-L",
"-n",
"-P",
"-s",
"-")):
262def _nested_command_index(arguments: list[str]) -> int:
263 """Return the actual executable index through reviewed command wrappers."""
264 if arguments
and arguments[0]
in {
"env",
"/usr/bin/env"}:
265 return _env_command_index(arguments)
266 if arguments
and arguments[0]
in {
"xargs",
"/usr/bin/xargs"}:
267 return _xargs_command_index(arguments)
271def _resolved_shell_argv(
273 variables: dict[str, str],
274 policies: Mapping[str, ShellPolicy],
275) -> tuple[list[str], bool]:
276 """Resolve leading declarations and scalar command indirection."""
277 tokens = list(segment)
278 while tokens
and tokens[0]
in SHELL_PREFIX:
280 if tokens
and tokens[0].startswith(
"@"):
281 tokens[0] = tokens[0][1:]
282 if not tokens
or tokens[0]
in NONCOMMAND_SHELL_PREFIX
or tokens[0]
in {
"}",
"]]"}:
284 declaration = bool(tokens
and tokens[0]
in DECLARATION_PREFIX)
287 while tokens
and re.match(
r"^[A-Za-z_][A-Za-z0-9_]*=", tokens[0]):
288 name, value = tokens.pop(0).split(
"=", 1)
289 variables[name] = _expand_token(value, variables)
292 active = [token
for token
in tokens
if token
not in {
"then",
"do",
"}"}]
293 if active
and active[0] ==
"exec":
295 command_index = _nested_command_index(active)
296 expanded = [_expand_token(token, variables)
for token
in active]
297 indirect_interpreter = (
298 command_index < len(active)
299 and active[command_index].startswith(
"$")
300 and expanded[command_index] ==
"/bin/bash"
302 indirect_target = any(
303 token.startswith(
"$")
305 and _target_for_token(value, policies)
is not None
306 for index, (token, value)
in enumerate(zip(active, expanded, strict=
True))
307 if index != command_index
309 return expanded[command_index:], indirect_interpreter
or indirect_target
312def _is_authenticated_release_loader(
314 caller_policy: ShellPolicy |
None,
318 """Recognize either exact policy-bound release-helper source call."""
319 grammar = RELEASE_LOADER_GRAMMARS.get(rel
or "")
322 expected, usage, executable, privileged_parent = grammar
323 expected_argv = tuple(shlex.split(expected.split(
" || ", 1)[0]))
325 caller_policy
is not None
326 and caller_policy.security
is ShellSecurity.PRIVILEGED
327 and caller_policy.usage
is usage
328 and caller_policy.dialect
is ShellDialect.BASH
329 and caller_policy.executable
is executable
330 and caller_policy.source_requires_privileged_parent
is privileged_parent
331 and logical.strip() == expected
332 and tuple(segment) == expected_argv
336def _scan_shell_segment(
338 context: ShellSegmentContext,
339) -> tuple[list[CallerFinding], int]:
340 """Validate one shell command segment and count an accepted loader call."""
341 is_loader_call = segment
and segment[0] == RELEASE_LOADER_NAME
342 is_loader_definition = context.stripped == f
"{RELEASE_LOADER_NAME}() {{"
343 if is_loader_call
and not is_loader_definition:
344 accepted = _is_authenticated_release_loader(
346 context.caller_policy,
353 else [CallerFinding(context.number,
"authenticated release loader drifted")]
355 return findings, int(accepted)
356 argv, indirect = _resolved_shell_argv(segment, context.variables, context.policies)
358 if _is_exact_reference(context.rel, context.logical):
361 CallerFinding(context.number,
"variable-indirect protected interpreter or target")
363 messages = validate_argv(argv, context.policies, context.caller_policy)
364 targets = {target
for token
in argv
if (target := _target_for_token(token, context.policies))}
368 and all(_is_exact_reference(context.rel, context.logical, target)
for target
in targets)
371 return [CallerFinding(context.number, message)
for message
in messages], 0
376 policies: Mapping[str, ShellPolicy],
377 caller_policy: ShellPolicy |
None,
378 rel: str |
None =
None,
379) -> list[CallerFinding]:
380 """Structurally validate shell logical commands."""
381 findings: list[CallerFinding] = []
382 variables: dict[str, str] = {}
383 governed = guarded_paths(policies)
385 release_loader_calls = 0
386 for number, logical
in _logical_shell_lines(text):
387 stripped = logical.strip()
389 array_depth += stripped.count(
"(") - stripped.count(
")")
392 r"^(?:declare\s+-a\s+|local\s+-a\s+)?[A-Za-z_][A-Za-z0-9_]*=\($",
397 tokens = _shell_tokens(logical)
399 targets = [target
for target
in governed
if target
in logical]
400 if targets
and not all(_is_exact_reference(rel, logical, target)
for target
in targets):
401 findings.append(CallerFinding(number,
"cannot parse governed shell occurrence"))
403 if tokens
and tokens[0]
in NONCOMMAND_SHELL_PREFIX:
405 context = ShellSegmentContext(
414 for segment
in _command_segments(tokens):
415 segment_findings, accepted_calls = _scan_shell_segment(segment, context)
416 findings.extend(segment_findings)
417 release_loader_calls += accepted_calls
420 if rel
not in RELEASE_LOADER_GRAMMARS
or release_loader_calls == 1
421 else (CallerFinding(0,
"authenticated release loader count drifted"),)
426def _call_name(call: ast.Call) -> str:
427 """Return a dotted static call name."""
428 parts: list[str] = []
429 node: ast.expr = call.func
430 while isinstance(node, ast.Attribute):
431 parts.append(node.attr)
433 if isinstance(node, ast.Name):
434 parts.append(node.id)
435 return ".".join(reversed(parts))
438def _string_expr(node: ast.AST, values: Mapping[str, object]) -> str |
None:
439 """Resolve a bounded static/path-like Python string expression."""
440 result: str |
None =
None
441 if isinstance(node, ast.Constant)
and isinstance(node.value, str):
443 elif isinstance(node, ast.Name):
444 value = values.get(node.id)
445 result = value
if isinstance(value, str)
else None
446 elif isinstance(node, ast.JoinedStr):
447 pieces: list[str] = []
448 for value
in node.values:
449 if isinstance(value, ast.Constant)
and isinstance(value.value, str):
450 pieces.append(value.value)
453 result =
"".join(pieces)
454 elif isinstance(node, ast.BinOp)
and isinstance(node.op, (ast.Add, ast.Div)):
455 left = _string_expr(node.left, values)
456 right = _string_expr(node.right, values)
459 if right
is not None:
460 separator =
"/" if isinstance(node.op, ast.Div)
else ""
461 result = f
"{left.rstrip('/')}{separator}{right.lstrip('/')}"
463 isinstance(node, ast.Call)
464 and isinstance(node.func, ast.Name)
465 and node.func.id
in {
"Path",
"PurePath",
"str"}
468 result = _string_expr(node.args[0], values)
472def _argv_expr(node: ast.AST, values: Mapping[str, object]) -> list[str] |
None:
473 """Resolve a bounded literal/list Python argv expression."""
474 if isinstance(node, ast.Name):
475 value = values.get(node.id)
476 return list(value)
if isinstance(value, tuple)
else None
477 if isinstance(node, (ast.List, ast.Tuple)):
479 for element
in node.elts:
480 if isinstance(element, ast.Starred):
481 nested = _argv_expr(element.value, values)
486 value = _string_expr(element, values)
491 if isinstance(node, ast.BinOp)
and isinstance(node.op, ast.Add):
492 left = _argv_expr(node.left, values)
493 right = _argv_expr(node.right, values)
494 return None if left
is None or right
is None else left + right
498class _PythonCallerVisitor(ast.NodeVisitor):
499 """Track bounded argv dataflow into process-creation APIs."""
501 def __init__(self, policies: Mapping[str, ShellPolicy]) ->
None:
502 self.policies = policies
503 self.values: dict[str, object] = {}
504 self.findings: list[CallerFinding] = []
506 def visit_Assign(self, node: ast.Assign) ->
None:
507 """Remember simple string and argv assignments."""
508 string = _string_expr(node.value, self.values)
509 argv = _argv_expr(node.value, self.values)
510 value: object |
None = tuple(argv)
if argv
is not None else string
511 for target
in node.targets:
512 if isinstance(target, ast.Name)
and value
is not None:
513 self.values[target.id] = value
514 self.generic_visit(node)
516 def visit_AnnAssign(self, node: ast.AnnAssign) ->
None:
517 """Remember simple annotated assignments."""
518 if isinstance(node.target, ast.Name)
and node.value
is not None:
519 string = _string_expr(node.value, self.values)
520 argv = _argv_expr(node.value, self.values)
521 value: object |
None = tuple(argv)
if argv
is not None else string
522 if value
is not None:
523 self.values[node.target.id] = value
524 self.generic_visit(node)
526 def visit_Call(self, node: ast.Call) ->
None:
527 """Validate bounded argv passed to process-creation APIs."""
528 name = _call_name(node)
529 if name
in PROCESS_CALLS
and node.args:
530 argv = _argv_expr(node.args[0], self.values)
531 if argv
is None and name.endswith(
"create_subprocess_exec"):
534 for argument
in node.args
535 if (value := _string_expr(argument, self.values))
is not None
537 if len(argv) != len(node.args):
539 governed_literals = {
541 for child
in ast.walk(node)
542 if isinstance(child, ast.Constant)
and isinstance(child.value, str)
543 for target
in guarded_paths(self.policies)
544 if target
in child.value
547 if governed_literals:
548 self.findings.append(
549 CallerFinding(node.lineno,
"governed Python process argv is unresolved")
552 for message
in validate_argv(argv, self.policies,
None):
553 self.findings.append(CallerFinding(node.lineno, message))
554 self.generic_visit(node)
557def scan_python_text(text: str, policies: Mapping[str, ShellPolicy]) -> list[CallerFinding]:
558 """Parse Python and validate process argv with bounded dataflow."""
560 tree = ast.parse(text)
561 except SyntaxError
as exc:
562 if any(target
in text
for target
in guarded_paths(policies)):
563 return [CallerFinding(exc.lineno
or 1,
"cannot parse governed Python occurrence")]
565 visitor = _PythonCallerVisitor(policies)
567 return visitor.findings
570def _json_argv_findings(
572 policies: Mapping[str, ShellPolicy],
574) -> list[CallerFinding]:
575 """Validate command/args objects and argv arrays recursively."""
576 findings: list[CallerFinding] = []
577 if isinstance(value, dict):
578 command = value.get(
"command")
579 args = value.get(
"args")
580 grouped = isinstance(command, str)
and isinstance(args, list)
582 argv = [command, *(item
for item
in args
if isinstance(item, str))]
583 if len(argv) != len(args) + 1:
584 findings.append(CallerFinding(line,
"JSON command argv contains non-string data"))
587 CallerFinding(line, message)
for message
in validate_argv(argv, policies,
None)
589 for key, nested
in value.items():
590 if grouped
and key
in {
"command",
"args"}:
592 findings.extend(_json_argv_findings(nested, policies, line))
593 elif isinstance(value, list):
594 if all(isinstance(item, str)
for item
in value):
596 if any(_target_for_token(item, policies)
is not None for item
in argv):
598 CallerFinding(line, message)
for message
in validate_argv(argv, policies,
None)
601 findings.extend(_json_argv_findings(nested, policies, line))
605def scan_json_text(text: str, policies: Mapping[str, ShellPolicy]) -> list[CallerFinding]:
606 """Parse JSON command structures and argv arrays."""
608 value = json.loads(text)
609 except json.JSONDecodeError
as exc:
610 if any(target
in text
for target
in guarded_paths(policies)):
611 return [CallerFinding(exc.lineno,
"cannot parse governed JSON occurrence")]
613 return _json_argv_findings(value, policies)
616def scan_markdown_text(text: str, policies: Mapping[str, ShellPolicy]) -> list[CallerFinding]:
617 """Validate executable shell and JSON fenced blocks; prose stays prose."""
618 findings: list[CallerFinding] = []
619 lines = text.splitlines()
621 while index < len(lines):
622 match = MARKDOWN_FENCE.match(lines[index])
626 fence, language = match.groups()
629 payload: list[str] = []
630 while index < len(lines)
and not lines[index].lstrip().startswith(fence):
631 payload.append(lines[index])
633 block =
"\n".join(payload) +
"\n"
634 if language
in SHELL_FENCE_LANGUAGES:
635 normalized =
"\n".join(line.removeprefix(
"$ ").removeprefix(
"> ")
for line
in payload)
636 block_findings = scan_shell_text(normalized, policies,
None)
637 elif language
in JSON_FENCE_LANGUAGES:
638 block_findings = scan_json_text(block, policies)
642 CallerFinding(start + finding.line - 1, finding.message)
for finding
in block_findings
648def scan_just_text(text: str, policies: Mapping[str, ShellPolicy]) -> list[CallerFinding]:
649 """Validate recipe bodies and preserve their exact interpreter context."""
650 findings: list[CallerFinding] = []
651 lines = text.splitlines()
653 match.group(1):
"/usr/bin/env -i"
657 r'^([A-Za-z_][A-Za-z0-9_]*)\s*:=\s*"/usr/bin/env -i(?:\s|\")',
663 while index < len(lines):
664 if not JUST_RECIPE.match(lines[index]):
668 body_start = index + 1
670 while index < len(lines)
and (
not lines[index]
or lines[index][0].isspace()):
671 body_line = lines[index].lstrip()
672 for name, prefix
in clean_prefixes.items():
673 body_line = body_line.replace(f
"{{{{ {name} }}}}", prefix)
674 body.append(body_line)
677 line
for line
in body
if line
and (line ==
"#!/bin/bash -p" or not line.startswith(
"#"))
680 if active
and active[0] ==
"#!/bin/bash -p":
681 caller_policy = ShellPolicy(
682 ShellSecurity.PRIVILEGED,
686 source_requires_privileged_parent=
False,
689 CallerFinding(body_start + finding.line - 1, finding.message)
690 for finding
in scan_shell_text(
"\n".join(body) +
"\n", policies, caller_policy)
695def scan_yaml_text(text: str, policies: Mapping[str, ShellPolicy]) -> list[CallerFinding]:
696 """Validate YAML command scalars and block bodies without accepting data keys."""
697 findings: list[CallerFinding] = []
698 lines = text.splitlines()
699 block_indent: int |
None =
None
701 block: list[str] = []
704 def normalize_templates(command: str) -> str:
705 """Resolve only quoted literal governed paths in Ansible expressions."""
707 for target
in guarded_paths(policies):
709 r"\{\{\s*\([A-Za-z_][A-Za-z0-9_]*\s*~\s*"
710 rf
"(['\"])/?{re.escape(target)}\1\)\s*\|\s*quote\s*\}}\}}"
712 normalized = re.sub(pattern, target, normalized)
718 command =
" ".join(line.strip()
for line
in block)
if block_folded
else "\n".join(block)
719 command = normalize_templates(command)
721 CallerFinding(block_start + finding.line - 1, finding.message)
722 for finding
in scan_shell_text(command +
"\n", policies,
None)
726 for number, line
in enumerate(lines, start=1):
727 indent = len(line) - len(line.lstrip())
728 if block_indent
is not None:
729 if line.strip()
and indent <= block_indent:
733 block.append(line[block_indent + 1 :]
if len(line) > block_indent
else "")
735 match = re.match(
r"^\s*(?:-\s*)?([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$", line)
736 if match
is None or match.group(1)
not in YAML_COMMAND_KEYS:
738 value = match.group(2).strip()
739 if value
in {
"|",
">",
"|-",
">-"}:
740 block_indent = indent
741 block_folded = value.startswith(
">")
742 block_start = number + 1
745 CallerFinding(number, finding.message)
746 for finding
in scan_shell_text(normalize_templates(value), policies,
None)
752def scan_dockerfile_text(text: str, policies: Mapping[str, ShellPolicy]) -> list[CallerFinding]:
753 """Validate Docker RUN shell commands and JSON-form CMD/ENTRYPOINT argv."""
754 findings: list[CallerFinding] = []
755 for number, logical
in _logical_shell_lines(text):
756 match = re.match(
r"^\s*([A-Za-z]+)\s+(.+)$", logical)
757 if match
is None or match.group(1).upper()
not in DOCKER_COMMANDS:
759 command, value = match.group(1).upper(), match.group(2).strip()
760 if command
in {
"CMD",
"ENTRYPOINT"}
and value.startswith(
"["):
762 CallerFinding(number, finding.message)
763 for finding
in scan_json_text(value, policies)
767 CallerFinding(number, finding.message)
768 for finding
in scan_shell_text(value, policies,
None)
776 policies: Mapping[str, ShellPolicy],
777) -> list[CallerFinding]:
778 """Dispatch one first-party executable/configuration text surface."""
779 findings: list[CallerFinding]
780 caller_policy = policies.get(rel)
781 if rel.endswith(
".just")
or rel ==
"justfile":
782 findings = scan_just_text(text, policies)
783 elif caller_policy
is not None:
784 findings = scan_shell_text(text, policies, caller_policy, rel)
785 elif rel.endswith(
".py"):
786 findings = scan_python_text(text, policies)
787 elif rel.endswith(
".json"):
788 findings = scan_json_text(text, policies)
789 elif rel.endswith(
".md"):
790 findings = scan_markdown_text(text, policies)
791 elif rel.endswith((
".yml",
".yaml")):
792 findings = scan_yaml_text(text, policies)
793 elif rel.endswith(
"Dockerfile")
or rel.rsplit(
"/", 1)[-1] ==
"Dockerfile":
794 findings = scan_dockerfile_text(text, policies)
795 elif rel ==
".env.example":
796 findings = scan_shell_text(text, policies,
None)
802def _static_reference_selftest_failures(policies: dict[str, ShellPolicy]) -> list[str]:
803 """Exercise privileged source edges and the exact syntax-only reference."""
804 sourced = fixtures.SOURCED
806 "privileged source parent rejected"
807 for prefix
in (
"source ",
". $ROOT/")
808 if scan_shell_text(f
"{prefix}{sourced}\n", policies, fixtures.PRIVILEGED_CALLER)
810 exact_static =
"/bin/bash -p -n scripts/dev/agent_workspace.sh\n"
813 "scripts/dev/agent_workspace.sh": ShellPolicy(
814 ShellSecurity.PRIVILEGED,
818 source_requires_privileged_parent=
False,
821 if scan_shell_text(exact_static, static_policy,
None,
"scripts/ci/gates/tests.sh"):
822 failures.append(
"exact reasoned static syntax reference was rejected")
823 if not scan_shell_text(
824 exact_static.replace(
" -n ",
" -n -O extglob "),
827 "scripts/ci/gates/tests.sh",
829 failures.append(
"mutated static syntax reference was accepted")
833def selftest_failures() -> tuple[list[str], int]:
834 """Replay the complete caller-bypass class in inert fixtures."""
835 policies = fixtures.POLICIES
836 shell_cases = fixtures.SHELL_CASES
839 for text, expected, label
in shell_cases
840 if len(scan_shell_text(text, policies,
None)) != expected
842 failures.extend(_static_reference_selftest_failures(policies))
843 loader_cases = fixtures.RELEASE_LOADER_CASES
846 for rel, caller, text, expect_finding, label
in loader_cases
847 if bool(scan_shell_text(f
"{text}\n", policies, caller, rel)) != expect_finding
849 python_cases = fixtures.PYTHON_CASES
852 for text, expected, label
in python_cases
853 if len(scan_python_text(text, policies)) != expected
856 "json": scan_json_text,
857 "yaml": scan_yaml_text,
858 "docker": scan_dockerfile_text,
859 "markdown": scan_markdown_text,
861 surface_cases = fixtures.SURFACE_CASES
864 for text, scanner, expected, label
in surface_cases
865 if len(surface_scanners[scanner](text, policies)) != expected
867 just_cases = fixtures.JUST_CASES
870 for text, expected, label
in just_cases
871 if len(scan_just_text(text, policies)) != expected