@biffo/cli 0.315.2 → 0.315.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_skeletons/plugin-template/scripts/py-dependency-audit-truncation.test.sh +272 -0
- package/_skeletons/plugin-template/scripts/py-dependency-audit.sh +14 -1
- package/_skeletons/sibling-template/scripts/py-dependency-audit-truncation.test.sh +272 -0
- package/_skeletons/sibling-template/scripts/py-dependency-audit.sh +14 -1
- package/package.json +1 -1
- package/scripts/pg-test-db.sh +74 -30
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
#
|
|
3
|
+
# Proves py-dependency-audit.sh (#1995) no longer pipes a large writer
|
|
4
|
+
# directly into `head -c N`. `head -c` closes its read end the instant it
|
|
5
|
+
# has enough bytes; if the upstream write is still in flight when that
|
|
6
|
+
# happens, the writer gets SIGPIPE. Confirmed live via `strace -f` against
|
|
7
|
+
# the real script's line-234 command (a >4000-byte jq-filtered finding
|
|
8
|
+
# dump): `jq`, forked from that exact pipeline, was silently killed by
|
|
9
|
+
# SIGPIPE the instant `head -c 4000` closed early on a large payload. That
|
|
10
|
+
# is the general mechanism #1995 reports. The CI-only "printf: printf: I/O
|
|
11
|
+
# error" wording it also reports needs dash's own builtin `printf` to be the
|
|
12
|
+
# process still writing at that instant -- a narrower timing window this
|
|
13
|
+
# workstation's dash/kernel did not reproduce across repeated attempts up to
|
|
14
|
+
# a 94MB payload (see the PR body for the full trail). The issue's own
|
|
15
|
+
# citation -- a real, timestamped CI job log
|
|
16
|
+
# (biffo-plugin-idea-scout PR #126) -- is the evidence for that exact
|
|
17
|
+
# wording; this file does not re-derive it.
|
|
18
|
+
#
|
|
19
|
+
# That same "I/O error" shape hit Case 1's own negative control for real, in
|
|
20
|
+
# guard-self-test-wiring.sh's CI run (34391849474, job 102607880682, head
|
|
21
|
+
# eeabce12): the runner's dash does not die to SIGPIPE the way this
|
|
22
|
+
# workstation's does -- its printf builtin catches the broken pipe, reports
|
|
23
|
+
# "printf: printf: I/O error" to stderr, returns a nonzero status, and lets
|
|
24
|
+
# the subshell keep running past it. Case 1's check used to recognise only
|
|
25
|
+
# the SIGPIPE-death shape (marker file absent) and treated any marker
|
|
26
|
+
# present as "the defect didn't reproduce" -- so on that runner it failed
|
|
27
|
+
# for the same reason the fix exists: an outcome the control's own author
|
|
28
|
+
# had not seen. The fix is to the control's DETECTION, not the mechanism
|
|
29
|
+
# under test: it accepts either manifestation of the same defect (killed
|
|
30
|
+
# outright, or survived with a reported failure) as reproduced.
|
|
31
|
+
#
|
|
32
|
+
# ## Case 1: deterministic proof of the pipe-shape defect and its fix
|
|
33
|
+
#
|
|
34
|
+
# Removes scheduling luck from the underlying mechanism instead of racing
|
|
35
|
+
# for it: the writer sleeps briefly before writing, so a reader that exits
|
|
36
|
+
# immediately is GUARANTEED to have already closed its read end before the
|
|
37
|
+
# writer's first write() call. Shows the OLD shape (writer piped straight
|
|
38
|
+
# into a reader that may close early) never gets through cleanly -- either
|
|
39
|
+
# the writer is killed before it can report anything, or it survives but its
|
|
40
|
+
# own write reports the broken pipe as a failure -- and the NEW shape this
|
|
41
|
+
# fix uses (write to a regular file first, read the FINISHED file) never
|
|
42
|
+
# does either, regardless of payload size or timing -- because there is no
|
|
43
|
+
# concurrent writer left for a reader to break.
|
|
44
|
+
#
|
|
45
|
+
# ## Case 2: the real script, end to end, with a >4000-byte finding
|
|
46
|
+
#
|
|
47
|
+
# Reuses the uv-stub harness from py-dependency-audit-classification.test.sh
|
|
48
|
+
# to run the actual target script against a finding payload whose filtered
|
|
49
|
+
# JSON exceeds the 4000-byte truncation threshold, and checks the
|
|
50
|
+
# classification/exit-code behaviour and the truncated dump are unaffected
|
|
51
|
+
# by the fix.
|
|
52
|
+
#
|
|
53
|
+
# Run: sh scripts/py-dependency-audit-truncation.test.sh
|
|
54
|
+
|
|
55
|
+
set -u
|
|
56
|
+
|
|
57
|
+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
58
|
+
TARGET="$SCRIPT_DIR/py-dependency-audit.sh"
|
|
59
|
+
|
|
60
|
+
WORK_DIR=$(mktemp -d)
|
|
61
|
+
trap 'rm -rf "$WORK_DIR"' EXIT
|
|
62
|
+
|
|
63
|
+
FAILURES=0
|
|
64
|
+
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# Case 0: the target script actually uses the safe shape
|
|
67
|
+
# ============================================================================
|
|
68
|
+
#
|
|
69
|
+
# Case 1 below proves the OLD shape is unsafe and the NEW shape is not, as a
|
|
70
|
+
# standalone fact about pipes -- it does not read $TARGET, so on its own it
|
|
71
|
+
# would not notice a regression that reverted the real script back to the
|
|
72
|
+
# vulnerable pattern. This case closes that gap directly against the file.
|
|
73
|
+
|
|
74
|
+
if grep -Eq "jq '\[\.dependencies\[\] \| select\(\.vulns \| length > 0\)\]' 2>/dev/null \| head -c" "$TARGET"; then
|
|
75
|
+
echo "FAIL: $TARGET pipes the vulns dump straight from jq into head -c again -- this is the exact shape #1995 reports"
|
|
76
|
+
FAILURES=$((FAILURES + 1))
|
|
77
|
+
else
|
|
78
|
+
echo "PASS: $TARGET does not pipe the vulns dump straight from jq into head -c"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
if grep -q 'head -c 4000 "\$dump_file"' "$TARGET"; then
|
|
82
|
+
echo "PASS: $TARGET truncates from a finished regular file, not a live pipe"
|
|
83
|
+
else
|
|
84
|
+
echo "FAIL: $TARGET no longer truncates from a regular file -- expected a 'head -c 4000 \"\$dump_file\"' line"
|
|
85
|
+
FAILURES=$((FAILURES + 1))
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# ============================================================================
|
|
89
|
+
# Case 1: deterministic pipe-shape proof
|
|
90
|
+
# ============================================================================
|
|
91
|
+
|
|
92
|
+
# 2MB of 'a' -- comfortably over the 64KiB Linux pipe buffer, so a writer
|
|
93
|
+
# emitting it in one shot cannot fit in the buffer and must block on a
|
|
94
|
+
# reader that may already be gone.
|
|
95
|
+
PAYLOAD="$(head -c 2000000 /dev/zero | tr '\0' 'a')"
|
|
96
|
+
|
|
97
|
+
# OLD shape: writer piped straight into an early-closing reader (`{ : ; }`
|
|
98
|
+
# reads nothing and exits immediately). The `sleep 0.5` guarantees the
|
|
99
|
+
# reader has already exited and closed its read end before the writer's
|
|
100
|
+
# first write() call -- this is not racing the scheduler, it is removing
|
|
101
|
+
# the race.
|
|
102
|
+
#
|
|
103
|
+
# The manifestation of "the writer did not survive" is NOT uniform across
|
|
104
|
+
# dash builds. On this workstation, the write gets SIGPIPE and the whole
|
|
105
|
+
# subshell dies mid-write, so it never reaches the `printf_rc` capture below
|
|
106
|
+
# and the marker file is simply absent -- that used to be the only shape
|
|
107
|
+
# this case checked for. But the CI runner's dash does NOT die to the
|
|
108
|
+
# signal: its printf builtin catches the broken pipe itself, prints
|
|
109
|
+
# "printf: printf: I/O error" to stderr, returns a NONZERO exit status, and
|
|
110
|
+
# the subshell keeps running past it -- there is no `&&`/`set -e` between
|
|
111
|
+
# these statements -- so the marker gets written anyway. Confirmed verbatim
|
|
112
|
+
# in run 34391849474 job 102607880682: line 91 (this line) is exactly where
|
|
113
|
+
# "I/O error" was reported, and the case still failed with "unexpectedly let
|
|
114
|
+
# the writer complete", because the old check only recognised the
|
|
115
|
+
# SIGPIPE-death shape and treated ANY marker as proof the writer silently
|
|
116
|
+
# succeeded -- including one written after printf itself had just reported
|
|
117
|
+
# failure.
|
|
118
|
+
#
|
|
119
|
+
# So the marker now carries printf's own exit status instead of a fixed
|
|
120
|
+
# "wrote", and either manifestation of the same underlying defect counts as
|
|
121
|
+
# reproduced: the writer is killed outright (marker absent), or it survives
|
|
122
|
+
# but printf reports the broken pipe as a failure (marker present with a
|
|
123
|
+
# nonzero code, or stderr non-empty). Only a marker present with an
|
|
124
|
+
# unqualified rc=0 and empty stderr -- meaning the write to an
|
|
125
|
+
# already-closed reader somehow succeeded without a trace -- fails to
|
|
126
|
+
# reproduce the defect this case exists to demonstrate.
|
|
127
|
+
old_marker="$WORK_DIR/old_marker"
|
|
128
|
+
old_stderr="$WORK_DIR/old_stderr"
|
|
129
|
+
(
|
|
130
|
+
sleep 0.5
|
|
131
|
+
printf '%s' "$PAYLOAD"
|
|
132
|
+
printf_rc=$?
|
|
133
|
+
echo "$printf_rc" > "$old_marker"
|
|
134
|
+
) | { : ; } 2>"$old_stderr"
|
|
135
|
+
|
|
136
|
+
old_rc=""
|
|
137
|
+
if [ -s "$old_marker" ]; then
|
|
138
|
+
old_rc=$(cat "$old_marker")
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
if [ ! -s "$old_marker" ]; then
|
|
142
|
+
echo "PASS: old pipe shape (writer | early-closing reader) kills the writer before it can report anything -- the defect #1995 reports, reproduced deterministically (writer terminated outright, e.g. by SIGPIPE)"
|
|
143
|
+
elif [ "$old_rc" != "0" ] || [ -s "$old_stderr" ]; then
|
|
144
|
+
echo "PASS: old pipe shape (writer | early-closing reader) lets the writer survive but its own write reports the broken pipe as a failure -- the defect #1995 reports, reproduced deterministically (printf exit=$old_rc, stderr: $(tr '\n' ' ' < "$old_stderr"))"
|
|
145
|
+
else
|
|
146
|
+
echo "FAIL: old pipe shape unexpectedly let the writer complete with no error of any kind -- the defect this case exists to demonstrate did not reproduce"
|
|
147
|
+
FAILURES=$((FAILURES + 1))
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# NEW shape: writer completes into a regular file first; the "reader" then
|
|
151
|
+
# reads the FINISHED file, with no concurrent writer left to break. Must
|
|
152
|
+
# always succeed, regardless of payload size or timing.
|
|
153
|
+
new_dump="$WORK_DIR/new_dump"
|
|
154
|
+
printf '%s' "$PAYLOAD" > "$new_dump"
|
|
155
|
+
new_write_rc=$?
|
|
156
|
+
: < "$new_dump"
|
|
157
|
+
new_read_rc=$?
|
|
158
|
+
|
|
159
|
+
if [ "$new_write_rc" -eq 0 ] && [ "$new_read_rc" -eq 0 ]; then
|
|
160
|
+
echo "PASS: new shape (write to a file, then read the finished file) always succeeds -- no live pipe for a reader to close early"
|
|
161
|
+
else
|
|
162
|
+
echo "FAIL: new shape unexpectedly failed (write rc=$new_write_rc, read rc=$new_read_rc)"
|
|
163
|
+
FAILURES=$((FAILURES + 1))
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# ============================================================================
|
|
167
|
+
# Case 2: the real script, end to end, against a >4000-byte finding
|
|
168
|
+
# ============================================================================
|
|
169
|
+
|
|
170
|
+
STUB_DIR="$WORK_DIR/stub"
|
|
171
|
+
REPO_DIR="$WORK_DIR/repo"
|
|
172
|
+
OUT_FILE="$WORK_DIR/out"
|
|
173
|
+
mkdir -p "$STUB_DIR" "$REPO_DIR"
|
|
174
|
+
|
|
175
|
+
cat > "$STUB_DIR/uv" <<'STUB'
|
|
176
|
+
#!/usr/bin/env sh
|
|
177
|
+
if [ "$1" = "run" ] && [ "$2" = "pip-audit" ]; then
|
|
178
|
+
cat "$PIPAUDIT_STUB_OUTPUT"
|
|
179
|
+
exit 0
|
|
180
|
+
fi
|
|
181
|
+
echo "uv stub: unexpected invocation: $*" >&2
|
|
182
|
+
exit 99
|
|
183
|
+
STUB
|
|
184
|
+
chmod +x "$STUB_DIR/uv"
|
|
185
|
+
|
|
186
|
+
( cd "$REPO_DIR" \
|
|
187
|
+
&& git init -q -b trunk \
|
|
188
|
+
&& git config user.email test@example.com \
|
|
189
|
+
&& git config user.name "Test" )
|
|
190
|
+
cat > "$REPO_DIR/uv.lock" <<'LOCK'
|
|
191
|
+
version = 1
|
|
192
|
+
requires-python = ">=3.13"
|
|
193
|
+
|
|
194
|
+
[[package]]
|
|
195
|
+
name = "pip"
|
|
196
|
+
version = "26.1.2"
|
|
197
|
+
source = { registry = "https://pypi.org/simple" }
|
|
198
|
+
LOCK
|
|
199
|
+
( cd "$REPO_DIR" && git add -A && git commit -q -m base )
|
|
200
|
+
( cd "$REPO_DIR" && git branch -q "origin/dev" )
|
|
201
|
+
|
|
202
|
+
# A single package carrying enough vulns that jq's filtered dump of it
|
|
203
|
+
# exceeds 4000 bytes -- the exact condition #1995 requires (small payloads
|
|
204
|
+
# never hit the truncation path at all).
|
|
205
|
+
{
|
|
206
|
+
printf '{"dependencies": [{"name": "pip", "version": "26.1.2", "vulns": ['
|
|
207
|
+
i=0
|
|
208
|
+
while [ "$i" -lt 60 ]; do
|
|
209
|
+
[ "$i" -gt 0 ] && printf ','
|
|
210
|
+
printf '{"id": "PYSEC-2026-%d", "fix_versions": ["26.2.1"], "description": "%s"}' \
|
|
211
|
+
"$i" "$(head -c 200 /dev/zero | tr '\0' 'x')"
|
|
212
|
+
i=$((i + 1))
|
|
213
|
+
done
|
|
214
|
+
printf ']}]}'
|
|
215
|
+
} > "$STUB_DIR/pip-audit-output.json"
|
|
216
|
+
|
|
217
|
+
filtered_size=$(cd "$REPO_DIR" && PATH="$STUB_DIR:$PATH" jq -c '[.dependencies[] | select(.vulns | length > 0)]' "$STUB_DIR/pip-audit-output.json" | wc -c | tr -d ' ')
|
|
218
|
+
if [ "$filtered_size" -le 4000 ]; then
|
|
219
|
+
echo "FAIL: test fixture's filtered JSON is only ${filtered_size} bytes -- must exceed 4000 to exercise the truncation path #1995 is about"
|
|
220
|
+
FAILURES=$((FAILURES + 1))
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
(
|
|
224
|
+
cd "$REPO_DIR" || exit 97
|
|
225
|
+
PATH="$STUB_DIR:$PATH"
|
|
226
|
+
export PATH
|
|
227
|
+
PIPAUDIT_STUB_OUTPUT="$STUB_DIR/pip-audit-output.json"
|
|
228
|
+
export PIPAUDIT_STUB_OUTPUT
|
|
229
|
+
GITHUB_BASE_REF="dev"
|
|
230
|
+
export GITHUB_BASE_REF
|
|
231
|
+
sh "$TARGET"
|
|
232
|
+
) >"$OUT_FILE" 2>&1
|
|
233
|
+
rc=$?
|
|
234
|
+
|
|
235
|
+
# pip@26.1.2 is unchanged between base and PR-head (#1673 classification),
|
|
236
|
+
# so this must NOT block, same as case 1 in
|
|
237
|
+
# py-dependency-audit-classification.test.sh.
|
|
238
|
+
if [ "$rc" -eq 0 ]; then
|
|
239
|
+
echo "PASS: real script exits 0 on a >4000-byte pre-existing finding"
|
|
240
|
+
else
|
|
241
|
+
echo "FAIL: real script exited ${rc}, expected 0 -- output:"
|
|
242
|
+
cat "$OUT_FILE"
|
|
243
|
+
FAILURES=$((FAILURES + 1))
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
if grep -qF "pre-existing" "$OUT_FILE"; then
|
|
247
|
+
echo "PASS: real script still classifies the finding as pre-existing"
|
|
248
|
+
else
|
|
249
|
+
echo "FAIL: real script's output does not mention 'pre-existing' -- output:"
|
|
250
|
+
cat "$OUT_FILE"
|
|
251
|
+
FAILURES=$((FAILURES + 1))
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
out_size=$(wc -c < "$OUT_FILE" | tr -d ' ')
|
|
255
|
+
# The whole run's combined output, not just the dump, so this is a loose
|
|
256
|
+
# ceiling -- it exists to catch a regression that stops truncating at all
|
|
257
|
+
# (e.g. the dump ever growing back to the full ~13KB filtered JSON).
|
|
258
|
+
if [ "$out_size" -lt 8000 ]; then
|
|
259
|
+
echo "PASS: real script's combined output (${out_size} bytes) stays bounded -- the dump is still truncated"
|
|
260
|
+
else
|
|
261
|
+
echo "FAIL: real script's combined output is ${out_size} bytes -- the truncation may no longer be applied"
|
|
262
|
+
FAILURES=$((FAILURES + 1))
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
echo
|
|
266
|
+
if [ "$FAILURES" -eq 0 ]; then
|
|
267
|
+
echo "py-dependency-audit-truncation.test.sh: all checks passed."
|
|
268
|
+
exit 0
|
|
269
|
+
else
|
|
270
|
+
echo "py-dependency-audit-truncation.test.sh: $FAILURES check(s) failed."
|
|
271
|
+
exit 1
|
|
272
|
+
fi
|
|
@@ -231,7 +231,20 @@ audit_deps() {
|
|
|
231
231
|
done < "$findings_file"
|
|
232
232
|
rm -f "$findings_file"
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
# Written to a regular file first, then truncated with `head -c` from
|
|
235
|
+
# that file rather than from a live pipe -- the same reason
|
|
236
|
+
# `findings_file` above does it. `head -c 4000` piped directly onto
|
|
237
|
+
# `jq`'s output closes its read end the instant it has enough bytes;
|
|
238
|
+
# if the filtered JSON exceeds 4000 bytes, jq (and on a big enough
|
|
239
|
+
# payload, the upstream `printf` still writing into jq's now-closed
|
|
240
|
+
# stdin) can get SIGPIPE and dash's builtin `printf` reports that as a
|
|
241
|
+
# stray "printf: I/O error" line into an otherwise-clean, passing log
|
|
242
|
+
# (#1995). Reading a finished, regular file has no concurrent writer
|
|
243
|
+
# to break, so there is no pipe left for `head` to close early on.
|
|
244
|
+
dump_file="$(mktemp)"
|
|
245
|
+
printf '%s' "$out" | jq '[.dependencies[] | select(.vulns | length > 0)]' 2>/dev/null > "$dump_file"
|
|
246
|
+
head -c 4000 "$dump_file"
|
|
247
|
+
rm -f "$dump_file"
|
|
235
248
|
|
|
236
249
|
if [ "$introduced_count" -gt 0 ]; then
|
|
237
250
|
echo "::error::${label}: ${introduced_count} vulnerability(ies) introduced or upgraded by this diff (${preexisting_count} more pre-existing, not counted against it)."
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
#
|
|
3
|
+
# Proves py-dependency-audit.sh (#1995) no longer pipes a large writer
|
|
4
|
+
# directly into `head -c N`. `head -c` closes its read end the instant it
|
|
5
|
+
# has enough bytes; if the upstream write is still in flight when that
|
|
6
|
+
# happens, the writer gets SIGPIPE. Confirmed live via `strace -f` against
|
|
7
|
+
# the real script's line-234 command (a >4000-byte jq-filtered finding
|
|
8
|
+
# dump): `jq`, forked from that exact pipeline, was silently killed by
|
|
9
|
+
# SIGPIPE the instant `head -c 4000` closed early on a large payload. That
|
|
10
|
+
# is the general mechanism #1995 reports. The CI-only "printf: printf: I/O
|
|
11
|
+
# error" wording it also reports needs dash's own builtin `printf` to be the
|
|
12
|
+
# process still writing at that instant -- a narrower timing window this
|
|
13
|
+
# workstation's dash/kernel did not reproduce across repeated attempts up to
|
|
14
|
+
# a 94MB payload (see the PR body for the full trail). The issue's own
|
|
15
|
+
# citation -- a real, timestamped CI job log
|
|
16
|
+
# (biffo-plugin-idea-scout PR #126) -- is the evidence for that exact
|
|
17
|
+
# wording; this file does not re-derive it.
|
|
18
|
+
#
|
|
19
|
+
# That same "I/O error" shape hit Case 1's own negative control for real, in
|
|
20
|
+
# guard-self-test-wiring.sh's CI run (34391849474, job 102607880682, head
|
|
21
|
+
# eeabce12): the runner's dash does not die to SIGPIPE the way this
|
|
22
|
+
# workstation's does -- its printf builtin catches the broken pipe, reports
|
|
23
|
+
# "printf: printf: I/O error" to stderr, returns a nonzero status, and lets
|
|
24
|
+
# the subshell keep running past it. Case 1's check used to recognise only
|
|
25
|
+
# the SIGPIPE-death shape (marker file absent) and treated any marker
|
|
26
|
+
# present as "the defect didn't reproduce" -- so on that runner it failed
|
|
27
|
+
# for the same reason the fix exists: an outcome the control's own author
|
|
28
|
+
# had not seen. The fix is to the control's DETECTION, not the mechanism
|
|
29
|
+
# under test: it accepts either manifestation of the same defect (killed
|
|
30
|
+
# outright, or survived with a reported failure) as reproduced.
|
|
31
|
+
#
|
|
32
|
+
# ## Case 1: deterministic proof of the pipe-shape defect and its fix
|
|
33
|
+
#
|
|
34
|
+
# Removes scheduling luck from the underlying mechanism instead of racing
|
|
35
|
+
# for it: the writer sleeps briefly before writing, so a reader that exits
|
|
36
|
+
# immediately is GUARANTEED to have already closed its read end before the
|
|
37
|
+
# writer's first write() call. Shows the OLD shape (writer piped straight
|
|
38
|
+
# into a reader that may close early) never gets through cleanly -- either
|
|
39
|
+
# the writer is killed before it can report anything, or it survives but its
|
|
40
|
+
# own write reports the broken pipe as a failure -- and the NEW shape this
|
|
41
|
+
# fix uses (write to a regular file first, read the FINISHED file) never
|
|
42
|
+
# does either, regardless of payload size or timing -- because there is no
|
|
43
|
+
# concurrent writer left for a reader to break.
|
|
44
|
+
#
|
|
45
|
+
# ## Case 2: the real script, end to end, with a >4000-byte finding
|
|
46
|
+
#
|
|
47
|
+
# Reuses the uv-stub harness from py-dependency-audit-classification.test.sh
|
|
48
|
+
# to run the actual target script against a finding payload whose filtered
|
|
49
|
+
# JSON exceeds the 4000-byte truncation threshold, and checks the
|
|
50
|
+
# classification/exit-code behaviour and the truncated dump are unaffected
|
|
51
|
+
# by the fix.
|
|
52
|
+
#
|
|
53
|
+
# Run: sh scripts/py-dependency-audit-truncation.test.sh
|
|
54
|
+
|
|
55
|
+
set -u
|
|
56
|
+
|
|
57
|
+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
58
|
+
TARGET="$SCRIPT_DIR/py-dependency-audit.sh"
|
|
59
|
+
|
|
60
|
+
WORK_DIR=$(mktemp -d)
|
|
61
|
+
trap 'rm -rf "$WORK_DIR"' EXIT
|
|
62
|
+
|
|
63
|
+
FAILURES=0
|
|
64
|
+
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# Case 0: the target script actually uses the safe shape
|
|
67
|
+
# ============================================================================
|
|
68
|
+
#
|
|
69
|
+
# Case 1 below proves the OLD shape is unsafe and the NEW shape is not, as a
|
|
70
|
+
# standalone fact about pipes -- it does not read $TARGET, so on its own it
|
|
71
|
+
# would not notice a regression that reverted the real script back to the
|
|
72
|
+
# vulnerable pattern. This case closes that gap directly against the file.
|
|
73
|
+
|
|
74
|
+
if grep -Eq "jq '\[\.dependencies\[\] \| select\(\.vulns \| length > 0\)\]' 2>/dev/null \| head -c" "$TARGET"; then
|
|
75
|
+
echo "FAIL: $TARGET pipes the vulns dump straight from jq into head -c again -- this is the exact shape #1995 reports"
|
|
76
|
+
FAILURES=$((FAILURES + 1))
|
|
77
|
+
else
|
|
78
|
+
echo "PASS: $TARGET does not pipe the vulns dump straight from jq into head -c"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
if grep -q 'head -c 4000 "\$dump_file"' "$TARGET"; then
|
|
82
|
+
echo "PASS: $TARGET truncates from a finished regular file, not a live pipe"
|
|
83
|
+
else
|
|
84
|
+
echo "FAIL: $TARGET no longer truncates from a regular file -- expected a 'head -c 4000 \"\$dump_file\"' line"
|
|
85
|
+
FAILURES=$((FAILURES + 1))
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# ============================================================================
|
|
89
|
+
# Case 1: deterministic pipe-shape proof
|
|
90
|
+
# ============================================================================
|
|
91
|
+
|
|
92
|
+
# 2MB of 'a' -- comfortably over the 64KiB Linux pipe buffer, so a writer
|
|
93
|
+
# emitting it in one shot cannot fit in the buffer and must block on a
|
|
94
|
+
# reader that may already be gone.
|
|
95
|
+
PAYLOAD="$(head -c 2000000 /dev/zero | tr '\0' 'a')"
|
|
96
|
+
|
|
97
|
+
# OLD shape: writer piped straight into an early-closing reader (`{ : ; }`
|
|
98
|
+
# reads nothing and exits immediately). The `sleep 0.5` guarantees the
|
|
99
|
+
# reader has already exited and closed its read end before the writer's
|
|
100
|
+
# first write() call -- this is not racing the scheduler, it is removing
|
|
101
|
+
# the race.
|
|
102
|
+
#
|
|
103
|
+
# The manifestation of "the writer did not survive" is NOT uniform across
|
|
104
|
+
# dash builds. On this workstation, the write gets SIGPIPE and the whole
|
|
105
|
+
# subshell dies mid-write, so it never reaches the `printf_rc` capture below
|
|
106
|
+
# and the marker file is simply absent -- that used to be the only shape
|
|
107
|
+
# this case checked for. But the CI runner's dash does NOT die to the
|
|
108
|
+
# signal: its printf builtin catches the broken pipe itself, prints
|
|
109
|
+
# "printf: printf: I/O error" to stderr, returns a NONZERO exit status, and
|
|
110
|
+
# the subshell keeps running past it -- there is no `&&`/`set -e` between
|
|
111
|
+
# these statements -- so the marker gets written anyway. Confirmed verbatim
|
|
112
|
+
# in run 34391849474 job 102607880682: line 91 (this line) is exactly where
|
|
113
|
+
# "I/O error" was reported, and the case still failed with "unexpectedly let
|
|
114
|
+
# the writer complete", because the old check only recognised the
|
|
115
|
+
# SIGPIPE-death shape and treated ANY marker as proof the writer silently
|
|
116
|
+
# succeeded -- including one written after printf itself had just reported
|
|
117
|
+
# failure.
|
|
118
|
+
#
|
|
119
|
+
# So the marker now carries printf's own exit status instead of a fixed
|
|
120
|
+
# "wrote", and either manifestation of the same underlying defect counts as
|
|
121
|
+
# reproduced: the writer is killed outright (marker absent), or it survives
|
|
122
|
+
# but printf reports the broken pipe as a failure (marker present with a
|
|
123
|
+
# nonzero code, or stderr non-empty). Only a marker present with an
|
|
124
|
+
# unqualified rc=0 and empty stderr -- meaning the write to an
|
|
125
|
+
# already-closed reader somehow succeeded without a trace -- fails to
|
|
126
|
+
# reproduce the defect this case exists to demonstrate.
|
|
127
|
+
old_marker="$WORK_DIR/old_marker"
|
|
128
|
+
old_stderr="$WORK_DIR/old_stderr"
|
|
129
|
+
(
|
|
130
|
+
sleep 0.5
|
|
131
|
+
printf '%s' "$PAYLOAD"
|
|
132
|
+
printf_rc=$?
|
|
133
|
+
echo "$printf_rc" > "$old_marker"
|
|
134
|
+
) | { : ; } 2>"$old_stderr"
|
|
135
|
+
|
|
136
|
+
old_rc=""
|
|
137
|
+
if [ -s "$old_marker" ]; then
|
|
138
|
+
old_rc=$(cat "$old_marker")
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
if [ ! -s "$old_marker" ]; then
|
|
142
|
+
echo "PASS: old pipe shape (writer | early-closing reader) kills the writer before it can report anything -- the defect #1995 reports, reproduced deterministically (writer terminated outright, e.g. by SIGPIPE)"
|
|
143
|
+
elif [ "$old_rc" != "0" ] || [ -s "$old_stderr" ]; then
|
|
144
|
+
echo "PASS: old pipe shape (writer | early-closing reader) lets the writer survive but its own write reports the broken pipe as a failure -- the defect #1995 reports, reproduced deterministically (printf exit=$old_rc, stderr: $(tr '\n' ' ' < "$old_stderr"))"
|
|
145
|
+
else
|
|
146
|
+
echo "FAIL: old pipe shape unexpectedly let the writer complete with no error of any kind -- the defect this case exists to demonstrate did not reproduce"
|
|
147
|
+
FAILURES=$((FAILURES + 1))
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# NEW shape: writer completes into a regular file first; the "reader" then
|
|
151
|
+
# reads the FINISHED file, with no concurrent writer left to break. Must
|
|
152
|
+
# always succeed, regardless of payload size or timing.
|
|
153
|
+
new_dump="$WORK_DIR/new_dump"
|
|
154
|
+
printf '%s' "$PAYLOAD" > "$new_dump"
|
|
155
|
+
new_write_rc=$?
|
|
156
|
+
: < "$new_dump"
|
|
157
|
+
new_read_rc=$?
|
|
158
|
+
|
|
159
|
+
if [ "$new_write_rc" -eq 0 ] && [ "$new_read_rc" -eq 0 ]; then
|
|
160
|
+
echo "PASS: new shape (write to a file, then read the finished file) always succeeds -- no live pipe for a reader to close early"
|
|
161
|
+
else
|
|
162
|
+
echo "FAIL: new shape unexpectedly failed (write rc=$new_write_rc, read rc=$new_read_rc)"
|
|
163
|
+
FAILURES=$((FAILURES + 1))
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# ============================================================================
|
|
167
|
+
# Case 2: the real script, end to end, against a >4000-byte finding
|
|
168
|
+
# ============================================================================
|
|
169
|
+
|
|
170
|
+
STUB_DIR="$WORK_DIR/stub"
|
|
171
|
+
REPO_DIR="$WORK_DIR/repo"
|
|
172
|
+
OUT_FILE="$WORK_DIR/out"
|
|
173
|
+
mkdir -p "$STUB_DIR" "$REPO_DIR"
|
|
174
|
+
|
|
175
|
+
cat > "$STUB_DIR/uv" <<'STUB'
|
|
176
|
+
#!/usr/bin/env sh
|
|
177
|
+
if [ "$1" = "run" ] && [ "$2" = "pip-audit" ]; then
|
|
178
|
+
cat "$PIPAUDIT_STUB_OUTPUT"
|
|
179
|
+
exit 0
|
|
180
|
+
fi
|
|
181
|
+
echo "uv stub: unexpected invocation: $*" >&2
|
|
182
|
+
exit 99
|
|
183
|
+
STUB
|
|
184
|
+
chmod +x "$STUB_DIR/uv"
|
|
185
|
+
|
|
186
|
+
( cd "$REPO_DIR" \
|
|
187
|
+
&& git init -q -b trunk \
|
|
188
|
+
&& git config user.email test@example.com \
|
|
189
|
+
&& git config user.name "Test" )
|
|
190
|
+
cat > "$REPO_DIR/uv.lock" <<'LOCK'
|
|
191
|
+
version = 1
|
|
192
|
+
requires-python = ">=3.13"
|
|
193
|
+
|
|
194
|
+
[[package]]
|
|
195
|
+
name = "pip"
|
|
196
|
+
version = "26.1.2"
|
|
197
|
+
source = { registry = "https://pypi.org/simple" }
|
|
198
|
+
LOCK
|
|
199
|
+
( cd "$REPO_DIR" && git add -A && git commit -q -m base )
|
|
200
|
+
( cd "$REPO_DIR" && git branch -q "origin/dev" )
|
|
201
|
+
|
|
202
|
+
# A single package carrying enough vulns that jq's filtered dump of it
|
|
203
|
+
# exceeds 4000 bytes -- the exact condition #1995 requires (small payloads
|
|
204
|
+
# never hit the truncation path at all).
|
|
205
|
+
{
|
|
206
|
+
printf '{"dependencies": [{"name": "pip", "version": "26.1.2", "vulns": ['
|
|
207
|
+
i=0
|
|
208
|
+
while [ "$i" -lt 60 ]; do
|
|
209
|
+
[ "$i" -gt 0 ] && printf ','
|
|
210
|
+
printf '{"id": "PYSEC-2026-%d", "fix_versions": ["26.2.1"], "description": "%s"}' \
|
|
211
|
+
"$i" "$(head -c 200 /dev/zero | tr '\0' 'x')"
|
|
212
|
+
i=$((i + 1))
|
|
213
|
+
done
|
|
214
|
+
printf ']}]}'
|
|
215
|
+
} > "$STUB_DIR/pip-audit-output.json"
|
|
216
|
+
|
|
217
|
+
filtered_size=$(cd "$REPO_DIR" && PATH="$STUB_DIR:$PATH" jq -c '[.dependencies[] | select(.vulns | length > 0)]' "$STUB_DIR/pip-audit-output.json" | wc -c | tr -d ' ')
|
|
218
|
+
if [ "$filtered_size" -le 4000 ]; then
|
|
219
|
+
echo "FAIL: test fixture's filtered JSON is only ${filtered_size} bytes -- must exceed 4000 to exercise the truncation path #1995 is about"
|
|
220
|
+
FAILURES=$((FAILURES + 1))
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
(
|
|
224
|
+
cd "$REPO_DIR" || exit 97
|
|
225
|
+
PATH="$STUB_DIR:$PATH"
|
|
226
|
+
export PATH
|
|
227
|
+
PIPAUDIT_STUB_OUTPUT="$STUB_DIR/pip-audit-output.json"
|
|
228
|
+
export PIPAUDIT_STUB_OUTPUT
|
|
229
|
+
GITHUB_BASE_REF="dev"
|
|
230
|
+
export GITHUB_BASE_REF
|
|
231
|
+
sh "$TARGET"
|
|
232
|
+
) >"$OUT_FILE" 2>&1
|
|
233
|
+
rc=$?
|
|
234
|
+
|
|
235
|
+
# pip@26.1.2 is unchanged between base and PR-head (#1673 classification),
|
|
236
|
+
# so this must NOT block, same as case 1 in
|
|
237
|
+
# py-dependency-audit-classification.test.sh.
|
|
238
|
+
if [ "$rc" -eq 0 ]; then
|
|
239
|
+
echo "PASS: real script exits 0 on a >4000-byte pre-existing finding"
|
|
240
|
+
else
|
|
241
|
+
echo "FAIL: real script exited ${rc}, expected 0 -- output:"
|
|
242
|
+
cat "$OUT_FILE"
|
|
243
|
+
FAILURES=$((FAILURES + 1))
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
if grep -qF "pre-existing" "$OUT_FILE"; then
|
|
247
|
+
echo "PASS: real script still classifies the finding as pre-existing"
|
|
248
|
+
else
|
|
249
|
+
echo "FAIL: real script's output does not mention 'pre-existing' -- output:"
|
|
250
|
+
cat "$OUT_FILE"
|
|
251
|
+
FAILURES=$((FAILURES + 1))
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
out_size=$(wc -c < "$OUT_FILE" | tr -d ' ')
|
|
255
|
+
# The whole run's combined output, not just the dump, so this is a loose
|
|
256
|
+
# ceiling -- it exists to catch a regression that stops truncating at all
|
|
257
|
+
# (e.g. the dump ever growing back to the full ~13KB filtered JSON).
|
|
258
|
+
if [ "$out_size" -lt 8000 ]; then
|
|
259
|
+
echo "PASS: real script's combined output (${out_size} bytes) stays bounded -- the dump is still truncated"
|
|
260
|
+
else
|
|
261
|
+
echo "FAIL: real script's combined output is ${out_size} bytes -- the truncation may no longer be applied"
|
|
262
|
+
FAILURES=$((FAILURES + 1))
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
echo
|
|
266
|
+
if [ "$FAILURES" -eq 0 ]; then
|
|
267
|
+
echo "py-dependency-audit-truncation.test.sh: all checks passed."
|
|
268
|
+
exit 0
|
|
269
|
+
else
|
|
270
|
+
echo "py-dependency-audit-truncation.test.sh: $FAILURES check(s) failed."
|
|
271
|
+
exit 1
|
|
272
|
+
fi
|
|
@@ -231,7 +231,20 @@ audit_deps() {
|
|
|
231
231
|
done < "$findings_file"
|
|
232
232
|
rm -f "$findings_file"
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
# Written to a regular file first, then truncated with `head -c` from
|
|
235
|
+
# that file rather than from a live pipe -- the same reason
|
|
236
|
+
# `findings_file` above does it. `head -c 4000` piped directly onto
|
|
237
|
+
# `jq`'s output closes its read end the instant it has enough bytes;
|
|
238
|
+
# if the filtered JSON exceeds 4000 bytes, jq (and on a big enough
|
|
239
|
+
# payload, the upstream `printf` still writing into jq's now-closed
|
|
240
|
+
# stdin) can get SIGPIPE and dash's builtin `printf` reports that as a
|
|
241
|
+
# stray "printf: I/O error" line into an otherwise-clean, passing log
|
|
242
|
+
# (#1995). Reading a finished, regular file has no concurrent writer
|
|
243
|
+
# to break, so there is no pipe left for `head` to close early on.
|
|
244
|
+
dump_file="$(mktemp)"
|
|
245
|
+
printf '%s' "$out" | jq '[.dependencies[] | select(.vulns | length > 0)]' 2>/dev/null > "$dump_file"
|
|
246
|
+
head -c 4000 "$dump_file"
|
|
247
|
+
rm -f "$dump_file"
|
|
235
248
|
|
|
236
249
|
if [ "$introduced_count" -gt 0 ]; then
|
|
237
250
|
echo "::error::${label}: ${introduced_count} vulnerability(ies) introduced or upgraded by this diff (${preexisting_count} more pre-existing, not counted against it)."
|
package/package.json
CHANGED
package/scripts/pg-test-db.sh
CHANGED
|
@@ -35,8 +35,9 @@
|
|
|
35
35
|
# `verify.sh` does: forks drift, and a per-instance copy of this would drift from
|
|
36
36
|
# the DDL layout it is meant to build. Everything instance-specific is DERIVED --
|
|
37
37
|
# the schema directories from `db/imports/*/`, the engine image from whether the
|
|
38
|
-
# DDL asks for PostGIS, and the did-it-build
|
|
39
|
-
#
|
|
38
|
+
# DDL asks for PostGIS, and the did-it-build check from what THIS repo's own
|
|
39
|
+
# last known-good build actually produced (see #2023 below), never from a
|
|
40
|
+
# guessed ratio. Nothing here names a product.
|
|
40
41
|
#
|
|
41
42
|
# ## Usage
|
|
42
43
|
#
|
|
@@ -557,20 +558,63 @@ fingerprint() {
|
|
|
557
558
|
|
|
558
559
|
WANT=$(fingerprint)
|
|
559
560
|
HAVE=""
|
|
561
|
+
HAVE_POLICIES=""
|
|
562
|
+
HAVE_MODULES=""
|
|
560
563
|
if [ "$RECREATE" -eq 0 ] &&
|
|
561
564
|
psql_admin -tAc "SELECT 1 FROM pg_database WHERE datname='$DB'" 2>/dev/null | grep -q 1; then
|
|
562
565
|
HAVE=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
563
566
|
-c "SELECT value FROM biffo_pg_test_fingerprint LIMIT 1" 2>/dev/null || true)
|
|
567
|
+
# policy_count/module_count were added alongside this guard (#2023). A row
|
|
568
|
+
# written before that migration has neither column, so this query fails
|
|
569
|
+
# (unknown column) rather than returning NULL, and `|| true` turns that
|
|
570
|
+
# failure into the same empty string a genuinely missing value would give --
|
|
571
|
+
# which is exactly what "unverifiable, so rebuild" needs below: it must read
|
|
572
|
+
# identically to "no stored count", never silently as "verified".
|
|
573
|
+
HAVE_POLICIES=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
574
|
+
-c "SELECT policy_count FROM biffo_pg_test_fingerprint LIMIT 1" 2>/dev/null || true)
|
|
575
|
+
HAVE_MODULES=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
576
|
+
-c "SELECT module_count FROM biffo_pg_test_fingerprint LIMIT 1" 2>/dev/null || true)
|
|
564
577
|
fi
|
|
565
578
|
|
|
579
|
+
# ── #2023: reuse must be verified against a known-good build, not a guess ────
|
|
580
|
+
#
|
|
581
|
+
# The old guard compared the live policy count to HALF of what the DDL
|
|
582
|
+
# *declares* (`grep -c CREATE POLICY`) -- a number a correct, complete build
|
|
583
|
+
# never reaches (521 declared vs. 362 produced on tabsii-platform, so the
|
|
584
|
+
# healthy ratio is ~0.70, not 1.0). A database missing a quarter of its schema
|
|
585
|
+
# passed it and was then blessed with a fingerprint every later run trusted.
|
|
586
|
+
#
|
|
587
|
+
# Worse, that check only ever ran on a fresh REBUILD (old section 4 below). The
|
|
588
|
+
# reuse path here never re-verified anything beyond the content hash matching --
|
|
589
|
+
# so a template that drifted after being blessed, by any means, was reused
|
|
590
|
+
# forever with no check of its own at all.
|
|
591
|
+
#
|
|
592
|
+
# The fix compares against reality instead of a guess: on every successful
|
|
593
|
+
# build (section 4), THIS script records the counts that build actually
|
|
594
|
+
# produced. A fingerprint match only says the DDL inputs are unchanged; it says
|
|
595
|
+
# nothing about whether the database in front of us still reflects what was
|
|
596
|
+
# built from them. So reuse additionally requires the live counts to equal the
|
|
597
|
+
# stored ones, exactly -- and a row with no stored counts (pre-#2023) is
|
|
598
|
+
# unverifiable, not innocent, so it rebuilds rather than being trusted.
|
|
566
599
|
if [ -n "$HAVE" ] && [ "$HAVE" = "$WANT" ]; then
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
600
|
+
if [ -z "$HAVE_POLICIES" ] || [ -z "$HAVE_MODULES" ]; then
|
|
601
|
+
say "fingerprint matches but this row predates stored policy/module counts - unverifiable, rebuilding rather than trusting it"
|
|
602
|
+
else
|
|
603
|
+
_live_policies=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
604
|
+
-c "SELECT count(*) FROM pg_policies" 2>/dev/null || echo 0)
|
|
605
|
+
_live_modules=0
|
|
606
|
+
[ -n "$DDL_FILES" ] && _live_modules=$(echo "$DDL_FILES" | wc -l | tr -d ' ')
|
|
607
|
+
if [ "${_live_policies:-0}" = "$HAVE_POLICIES" ] && [ "$_live_modules" = "$HAVE_MODULES" ]; then
|
|
608
|
+
say "schema is current ($_live_policies policies, $_live_modules modules), reusing $DB"
|
|
609
|
+
clone_for_this_run
|
|
610
|
+
emit
|
|
611
|
+
exit 0
|
|
612
|
+
fi
|
|
613
|
+
say "fingerprint matches but live schema diverged from its own record (have $_live_policies policies/$_live_modules modules, recorded $HAVE_POLICIES/$HAVE_MODULES) - rebuilding rather than reusing a partial schema"
|
|
614
|
+
fi
|
|
571
615
|
fi
|
|
572
616
|
|
|
573
|
-
[ -n "$HAVE" ] && say "schema inputs changed - rebuilding rather than serving a stale schema"
|
|
617
|
+
[ -n "$HAVE" ] && [ "$HAVE" != "$WANT" ] && say "schema inputs changed - rebuilding rather than serving a stale schema"
|
|
574
618
|
|
|
575
619
|
# --- 3. rebuild the way the app and CI do ------------------------------------
|
|
576
620
|
say "rebuilding $DB"
|
|
@@ -589,6 +633,7 @@ if [ -n "$ALEMBIC_DIR" ]; then
|
|
|
589
633
|
say "alembic upgrade head"
|
|
590
634
|
fi
|
|
591
635
|
|
|
636
|
+
_module_count=0
|
|
592
637
|
if [ -n "$DDL_FILES" ]; then
|
|
593
638
|
# ONE psql session, sorted by filename, mirroring the API's own DDL import.
|
|
594
639
|
# Session state an early module sets -- typically `SET search_path` in the
|
|
@@ -598,38 +643,37 @@ if [ -n "$DDL_FILES" ]; then
|
|
|
598
643
|
# shellcheck disable=SC2046
|
|
599
644
|
psql -q -v ON_ERROR_STOP=1 -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
600
645
|
--single-transaction $(echo "$DDL_FILES" | sed 's/^/-f /' | tr '\n' ' ') >/dev/null
|
|
601
|
-
|
|
646
|
+
_module_count=$(echo "$DDL_FILES" | wc -l | tr -d ' ')
|
|
647
|
+
say "$_module_count DDL modules applied"
|
|
602
648
|
fi
|
|
603
649
|
|
|
604
|
-
# --- 4.
|
|
605
|
-
#
|
|
606
|
-
#
|
|
607
|
-
#
|
|
608
|
-
#
|
|
609
|
-
#
|
|
650
|
+
# --- 4. record what this build actually produced (#2023) ---------------------
|
|
651
|
+
#
|
|
652
|
+
# No guessed threshold here anymore. The DDL apply above is one
|
|
653
|
+
# `--single-transaction`, `ON_ERROR_STOP=1` psql session, so a build that fails
|
|
654
|
+
# partway already aborts the whole script before reaching this line -- it does
|
|
655
|
+
# not limp to here half-applied. What DOES reach here is simply recorded, and
|
|
656
|
+
# it is that RECORD -- this build's own live counts, not a guess at what they
|
|
657
|
+
# "should" be -- that the reuse guard above checks every later database
|
|
658
|
+
# against. See the comment there for why a guessed ratio (521 declared, 362
|
|
659
|
+
# produced) blessed a schema missing a quarter of itself.
|
|
660
|
+
_policy_count=0
|
|
610
661
|
if [ -n "$DDL_FILES" ]; then
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
_actual=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
615
|
-
-c "SELECT count(*) FROM pg_policies" 2>/dev/null || echo 0)
|
|
616
|
-
if [ "${_actual:-0}" -lt $((_declared / 2)) ]; then
|
|
617
|
-
say "only ${_actual:-0} policies present against $_declared declared - the schema did not build."
|
|
618
|
-
say "Not recording a fingerprint; fix the DDL and re-run."
|
|
619
|
-
exit 1
|
|
620
|
-
fi
|
|
621
|
-
say "$_actual RLS policies ($_declared declared)"
|
|
622
|
-
fi
|
|
662
|
+
_policy_count=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
663
|
+
-c "SELECT count(*) FROM pg_policies" 2>/dev/null || echo 0)
|
|
664
|
+
say "$_policy_count RLS policies, $_module_count DDL modules applied"
|
|
623
665
|
fi
|
|
624
666
|
|
|
625
667
|
psql_db \
|
|
626
668
|
-c "CREATE TABLE IF NOT EXISTS biffo_pg_test_fingerprint (value text primary key)" \
|
|
669
|
+
-c "ALTER TABLE biffo_pg_test_fingerprint ADD COLUMN IF NOT EXISTS policy_count integer" \
|
|
670
|
+
-c "ALTER TABLE biffo_pg_test_fingerprint ADD COLUMN IF NOT EXISTS module_count integer" \
|
|
627
671
|
-c "TRUNCATE biffo_pg_test_fingerprint" \
|
|
628
|
-
-c "INSERT INTO biffo_pg_test_fingerprint (value) VALUES ('$WANT')" >/dev/null
|
|
672
|
+
-c "INSERT INTO biffo_pg_test_fingerprint (value, policy_count, module_count) VALUES ('$WANT', $_policy_count, $_module_count)" >/dev/null
|
|
629
673
|
|
|
630
|
-
# Only now, with the fingerprint recorded against a
|
|
631
|
-
#
|
|
632
|
-
# half-built database and record the failure against whoever ran next.
|
|
674
|
+
# Only now, with the fingerprint and its counts recorded against a build that
|
|
675
|
+
# actually completed, is the template fit to copy. Cloning earlier would hand
|
|
676
|
+
# out a half-built database and record the failure against whoever ran next.
|
|
633
677
|
clone_for_this_run
|
|
634
678
|
|
|
635
679
|
say "ready"
|