diff --git a/formatters/sh b/formatters/sh index 46006b3..1121439 100755 --- a/formatters/sh +++ b/formatters/sh @@ -89,15 +89,6 @@ def count_top_level_commands(s): return separators + 1 for i, line in enumerate(lines, 1): - # SETE-2: sh/bash -c with 2+ ;- or newline-separated commands and no set -e inside - m = re.search(r'\b(?:sh|bash)\s+-c\s+(["\'])([^"\']*)\1', line) - if m: - s = m.group(2) - if not re.search(r'(^|[^A-Za-z0-9_])set\s+-[A-Za-z]*[eE]', s): - n = count_top_level_commands(s) - if n >= 2: - findings.append((i, "SETE-2 sh/bash -c with " + str(n) + " ;-separated commands and no 'set -e' inside; container exit reflects only the last command")) - # LEAK-1: command substitution reading a secret-named file, inside echo/printf/die/error/fail sink_match = SINK_RE.search(line) if sink_match: @@ -107,6 +98,24 @@ for i, line in enumerate(lines, 1): if SECRET_RE.search(var): findings.append((i, "LEAK-1 secret variable '$" + var + "' read into string passed to '" + cmd + "'; could print credential document")) +# SETE-2 runs over the WHOLE source, not line by line: the shape that motivated +# this check spans lines — +# sh -c " +# hugo mod get ... +# hugo mod tidy +# " +# — so a per-line regex requiring both quotes on one line never matched it. +for m in re.finditer(r'\b(?:sh|bash)\s+-c\s+(["\'])(.*?)\1', content, re.DOTALL): + body = m.group(2) + if re.search(r'(^|[^A-Za-z0-9_])set\s+-[A-Za-z]*[eE]', body): + continue + n = count_top_level_commands(body) + if n < 2: + n = len([ln for ln in body.splitlines() if ln.strip() and not ln.strip().startswith('#')]) + if n >= 2: + line_no = content[:m.start()].count('\n') + 1 + findings.append((line_no, "SETE-2 sh/bash -c with " + str(n) + " commands and no 'set -e' inside; exit status reflects only the last command")) + for line_no, msg in findings: print(filepath + ":" + str(line_no) + ": " + msg) diff --git a/formatters/tests/test-sh-formatter.sh b/formatters/tests/test-sh-formatter.sh index 4a4574f..2d4bea5 100755 --- a/formatters/tests/test-sh-formatter.sh +++ b/formatters/tests/test-sh-formatter.sh @@ -234,6 +234,29 @@ else fail "missing-tool warning" "expected exit 0 + warning line, got exit=$rc stderr=$stderr_content" fi +# SETE-2 must see a MULTI-LINE `sh -c "..."`. That is the shape of the real +# sync-deps bug; the original per-line regex required both quotes on one line +# and missed it entirely. +f="$TMPDIR/sete2-multiline.sh" +printf '#!/usr/bin/env bash\nset -euo pipefail\ndocker run --rm img sh -c "\n hugo mod get a b c\n hugo mod tidy\n"\n' > "$f" +out="$(bash "$FORMATTER" "$f" 2>&1)" +if printf '%s' "$out" | grep -q "SETE-2"; then + pass "SETE-2 flags a multi-line sh -c with no set -e" +else + fail "SETE-2 MISSED the multi-line sh -c form (the real sync-deps bug); got: $out" +fi + +f="$TMPDIR/sete2-multiline-ok.sh" +printf '#!/usr/bin/env bash\nset -euo pipefail\ndocker run --rm img sh -c "\n set -e\n hugo mod get a b c\n hugo mod tidy\n"\n' > "$f" +out="$(bash "$FORMATTER" "$f" 2>&1)" +if printf '%s' "$out" | grep -q "SETE-2"; then + fail "SETE-2 false-positived on a multi-line sh -c that DOES set -e" +else + pass "SETE-2 quiet when the multi-line sh -c sets -e" +fi + + + # =========================================================================== echo "" echo "Results: $PASS passed, $FAIL failed"