Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 65 additions & 10 deletions scripts/propagate-hypatia-caller-id.sh
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,28 @@
return 0
fi

# The caller job is the one whose `uses:` names the estate's scan reusable.
caller="$(awk '
/^[[:space:]]*[A-Za-z0-9_.-]+:[[:space:]]*$/ { key=$1; sub(/:$/, "", key) }
/^[[:space:]]+uses:.*hypatia-scan-reusable\.ya?ml@/ { print key; exit }
# The caller job is the job whose `uses:` names the estate's scan reusable.
# Detect it structurally: the nearest job key — a line indented exactly two
# spaces whose value begins with `key:` (an optional trailing comment is
# allowed) — above the reusable `uses:` line.
#
# Why not "the last bare `key:` line above the call": that heuristic mistook a
# job-level `permissions:` block for the job key (three live repositories have
# one) and rewrote the permissions key itself. It also picked up `jobs:` when
# the job key carried a trailing comment. Both shapes now have fixtures.
uses_line="$(awk '
/^[[:space:]]*uses:.*hypatia-scan-reusable\.ya?ml@/ { print NR; exit }
' "$wf")"
caller=""; caller_key_line=""
if [ -n "$uses_line" ]; then

Check failure on line 152 in scripts/propagate-hypatia-caller-id.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDBG4JH1g4fQPItkbto&open=AaDBG4JH1g4fQPItkbto&pullRequest=879
caller_info="$(awk -v upto="$uses_line" '
NR < upto && /^ [A-Za-z0-9_.-]+:[[:space:]]*(#.*)?$/ { k=$1; sub(/:$/, "", k); ln=NR }
END { if (ln) print ln, k }
' "$wf")"
caller_key_line="${caller_info%% *}"
caller="${caller_info##* }"
[ "$caller_info" = "$caller_key_line" ] && { caller=""; caller_key_line=""; }

Check failure on line 159 in scripts/propagate-hypatia-caller-id.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDBG4JH1g4fQPItkbtp&open=AaDBG4JH1g4fQPItkbtp&pullRequest=879
fi

if [ -z "$caller" ]; then
printf '%s\t%s\t-\tunchanged-shape\n' "$(basename "$repo")" ".github/workflows/hypatia-scan.yml"
Expand All @@ -161,14 +178,52 @@
fi

if [ "$verdict" = "stage" ] && [ "$MODE_FIX" = 1 ]; then
# Rewrite only the job key line: same indentation, same position.
# Rewrite only the detected job key line, in place: same indentation, same
# position, any trailing comment preserved. Addressed by line number, so a
# nested `permissions:` key, a comment, or any other `scan:`-shaped line
# elsewhere in the file cannot be touched by accident.
local tmp; tmp="$(mktemp)"
awk -v from="$caller" -v to="$CANONICAL" '
BEGIN { done = 0 }
!done && $0 ~ "^[[:space:]]*" from ":[[:space:]]*$" { sub(from ":", to ":"); done = 1 }
awk -v n="$caller_key_line" -v to="$CANONICAL" '
NR == n {
match($0, /^[[:space:]]*/)
ind = substr($0, 1, RLENGTH)
rest = substr($0, RLENGTH + 1)
sub(/^[A-Za-z0-9_.-]+:/, to ":", rest)
$0 = ind rest
}
{ print }
' "$wf" > "$tmp" && mv "$tmp" "$wf"
verdict="staged"
' "$wf" > "$tmp"
# Preserve the file's trailing-newline state: a wrapper that ends without a
# newline must not gain one, so the diff stays a single-line replacement.
if [ -n "$(tail -c 1 "$wf" || true)" ]; then

Check failure on line 198 in scripts/propagate-hypatia-caller-id.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDBG4JH1g4fQPItkbtq&open=AaDBG4JH1g4fQPItkbtq&pullRequest=879
printf '%s' "$(cat "$tmp")" > "$tmp.trimmed" && mv "$tmp.trimmed" "$tmp"
fi
# Accept only a one-line replacement in place: same number of lines, exactly
# one differing line, that line is the detected key line, and it now carries
# the canonical id. Anything else is refused and the file is left untouched.
report="$(awk -v n="$caller_key_line" -v want="$CANONICAL" '
NR == FNR { old[FNR] = $0; nold = FNR; next }
{ new[FNR] = $0; nnew = FNR }
END {
if (nold != nnew) { printf "line count %d -> %d", nold, nnew; exit }
changed = 0; elsewhere = 0
for (i = 1; i <= nnew; i++) {
if (old[i] != new[i]) { changed++; if (i != n) elsewhere = 1 }
}
if (changed != 1) { printf "%d lines changed", changed; exit }
if (elsewhere) { printf "the changed line is not the key line"; exit }
if (new[n] !~ ("^ " want ":")) { printf "the key line is not canonical"; exit }
printf "ok"
}
' "$wf" "$tmp")"
if [ "$report" = "ok" ]; then

Check failure on line 219 in scripts/propagate-hypatia-caller-id.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDBG4JH1g4fQPItkbtr&open=AaDBG4JH1g4fQPItkbtr&pullRequest=879
mv "$tmp" "$wf"
verdict="staged"
else
rm -f "$tmp"
verdict="unchanged-shape"
note "$(basename "$repo"): rewrite refused — $report"
fi
fi

printf '%s\t%s\t%s\t%s\n' "$(basename "$repo")" ".github/workflows/hypatia-scan.yml" "$caller" "$verdict"
Expand Down
84 changes: 84 additions & 0 deletions scripts/tests/propagate-hypatia-caller-id-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,90 @@
check "fix-keeps-secrets-inherit" "1" "$(printf '%s\n' "$after_safe" | grep -c '^[[:space:]]*secrets: inherit$')"
check "fix-is-idempotent" "canonical" "$(HYPATIA_REQUIRED_JSON="$work/safe.json" bash "$SCRIPT" "$work/fleet" 2>/dev/null | awk -F'\t' '$1=="bbb-needs-rename"{print $4}')"

# --- shapes seen in the live estate, which the first detection missed -------

# ggg: a job-level `permissions:` block inside the caller job. The old
# "last bare `key:` line" heuristic reported the caller as `permissions`
# and rewrote the permissions key itself.
mkdir -p "$work/fleet/ggg-job-permissions/.github/workflows"
cat > "$work/fleet/ggg-job-permissions/.github/workflows/hypatia-scan.yml" <<'EOF'
name: Hypatia Security Scan
on:
push:
pull_request:

permissions:
actions: read
contents: read

jobs:
scan:
permissions:
actions: read
contents: read
security-events: write
uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@210f14e753c80064ec1bcae72f1d654dd9b0e687
secrets: inherit
EOF

# hhh: the job key carries a trailing comment. The old heuristic skipped it (the
# line is not a "bare" key) and picked up `jobs:` instead.
mkdir -p "$work/fleet/hhh-commented-key/.github/workflows"
cat > "$work/fleet/hhh-commented-key/.github/workflows/hypatia-scan.yml" <<'EOF'
name: Hypatia Security Scan
on: [push]

jobs:
scan: # estate scan gate
uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@210f14e753c80064ec1bcae72f1d654dd9b0e687
secrets: inherit
EOF

# iii: no trailing newline on the last line. The rewrite must still be a
# single-line replacement (a `\ No newline at end of file` marker is not
# a changed line).
mkdir -p "$work/fleet/iii-no-newline/.github/workflows"
printf '%s' 'name: Hypatia Security Scan
on: [push]

jobs:
scan:
uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@8f2ee50841e216cd8c192eeb68953118190f105c' > "$work/fleet/iii-no-newline/.github/workflows/hypatia-scan.yml"

job_key() { # the job key governing the reusable call, detected structurally

Check warning on line 153 in scripts/tests/propagate-hypatia-caller-id-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDBG4Go1g4fQPItkbtj&open=AaDBG4Go1g4fQPItkbtj&pullRequest=879
local n; n="$(awk '/^[[:space:]]*uses:.*hypatia-scan-reusable\.ya?ml@/ { print NR; exit }' "$1")"
awk -v n="$n" 'NR<n && /^ [A-Za-z0-9_.-]+:[[:space:]]*(#.*)?$/ { k=$1; sub(/:$/,"",k) } END { print k }' "$1"

Check warning on line 155 in scripts/tests/propagate-hypatia-caller-id-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDBG4Go1g4fQPItkbti&open=AaDBG4Go1g4fQPItkbti&pullRequest=879
}
changed_lines() { diff <(printf '%s\n' "$1") <(printf '%s\n' "$2") | grep -c '^[<>]'; }

Check warning on line 157 in scripts/tests/propagate-hypatia-caller-id-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDBG4Go1g4fQPItkbtk&open=AaDBG4Go1g4fQPItkbtk&pullRequest=879

Check warning on line 157 in scripts/tests/propagate-hypatia-caller-id-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDBG4Go1g4fQPItkbtm&open=AaDBG4Go1g4fQPItkbtm&pullRequest=879

Check warning on line 157 in scripts/tests/propagate-hypatia-caller-id-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDBG4Go1g4fQPItkbtl&open=AaDBG4Go1g4fQPItkbtl&pullRequest=879

before_ggg="$(cat "$work/fleet/ggg-job-permissions/.github/workflows/hypatia-scan.yml")"
before_hhh="$(cat "$work/fleet/hhh-commented-key/.github/workflows/hypatia-scan.yml")"
before_iii="$(cat "$work/fleet/iii-no-newline/.github/workflows/hypatia-scan.yml")"

out="$(HYPATIA_REQUIRED_JSON="$work/safe.json" bash "$SCRIPT" --fix "$work/fleet" 2>/dev/null)"
check "job-level-permissions: the caller is the job key, not permissions" "scan" \
"$(printf '%s\n' "$out" | awk -F'\t' '$1=="ggg-job-permissions"{print $3}')"
check "job-level-permissions: staged" "staged" \
"$(printf '%s\n' "$out" | awk -F'\t' '$1=="ggg-job-permissions"{print $4}')"
check "job-level-permissions: job key renamed" "hypatia" \
"$(job_key "$work/fleet/ggg-job-permissions/.github/workflows/hypatia-scan.yml")"
check "job-level-permissions: nested permissions block untouched" "1" \
"$(grep -c '^ permissions:$' "$work/fleet/ggg-job-permissions/.github/workflows/hypatia-scan.yml")"
check "job-level-permissions: one-line replacement" "2" \
"$(changed_lines "$before_ggg" "$(cat "$work/fleet/ggg-job-permissions/.github/workflows/hypatia-scan.yml")")"
check "commented-key: job key renamed" "hypatia" \
"$(job_key "$work/fleet/hhh-commented-key/.github/workflows/hypatia-scan.yml")"
check "commented-key: trailing comment preserved" "1" \
"$(grep -c '^ hypatia: # estate scan gate$' "$work/fleet/hhh-commented-key/.github/workflows/hypatia-scan.yml")"
check "commented-key: one-line replacement" "2" \
"$(changed_lines "$before_hhh" "$(cat "$work/fleet/hhh-commented-key/.github/workflows/hypatia-scan.yml")")"
check "no-newline: job key renamed" "hypatia" \

Check warning on line 180 in scripts/tests/propagate-hypatia-caller-id-test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of using the literal 'hypatia' 4 times.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaDBG4Go1g4fQPItkbtn&open=AaDBG4Go1g4fQPItkbtn&pullRequest=879
"$(job_key "$work/fleet/iii-no-newline/.github/workflows/hypatia-scan.yml")"
check "no-newline: one-line replacement" "2" \
"$(changed_lines "$before_iii" "$(cat "$work/fleet/iii-no-newline/.github/workflows/hypatia-scan.yml")")"
check "fix-is-idempotent-for-the-new-shapes" "canonical" \
"$(HYPATIA_REQUIRED_JSON="$work/safe.json" bash "$SCRIPT" "$work/fleet" 2>/dev/null | awk -F'\t' '$1=="ggg-job-permissions"{print $4}')"

# Unreadable requirements: never rename on an unknown requirement set.
wrapper "$work/fleet/fff-unverified" scan
out="$(bash "$SCRIPT" --no-network "$work/fleet" 2>/dev/null)"
Expand Down
Loading