@codyswann/lisa 4.26.1 → 4.26.2
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/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +64 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts +5 -0
- package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +8 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +6 -5
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +64 -1
- package/expo/create-only/.github/workflows/nightly-e2e-report.yml +21 -6
- package/package.json +3 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +64 -1
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +64 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +64 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +64 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/parity-safety-net.sh +64 -1
- package/scripts/generate-nightly-e2e-guard-certificate.mjs +17 -1
- package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +238 -44
|
@@ -1035,6 +1035,69 @@ fi
|
|
|
1035
1035
|
# purpose, and blocking those would be the collision that gets a guard switched
|
|
1036
1036
|
# off. The trailing `([/[:space:]'"$qc"']|$)` is what draws that line.
|
|
1037
1037
|
readonly GIT_CONTROL_PLANE='(^|[[:space:]='"$qc"'./])\.git([/[:space:]'"$qc"']|$)'
|
|
1038
|
+
# This guard is the one segment walk that pairs TWO facts found in different
|
|
1039
|
+
# places: the protected target (`.git`) and the destructive verb (a recursive
|
|
1040
|
+
# forced delete). That makes its segmentation load-bearing in a way the rm and
|
|
1041
|
+
# push walks' is not — those two read a verb and its own operands, which are
|
|
1042
|
+
# always in one pipeline stage, so splitting on `|` costs them nothing.
|
|
1043
|
+
#
|
|
1044
|
+
# Here it cost everything. The old feeder was `tr '&|;' '\n'`, which cut a
|
|
1045
|
+
# PIPELINE into pieces before the pairing question was asked, and a pipeline is
|
|
1046
|
+
# exactly how a delete reaches a target named upstream of it:
|
|
1047
|
+
# `find .git -type f -print0 | xargs -0 rm -rf` became `find .git -type f
|
|
1048
|
+
# -print0` (a target, no verb) and `xargs -0 rm -rf` (a verb, no target), and
|
|
1049
|
+
# neither half could answer. Measured before this fix, with the blanket
|
|
1050
|
+
# `xargs rm -rf` rule masking the reported shape: `find .git -type d | parallel
|
|
1051
|
+
# rm -rf`, `… | /usr/bin/xargs rm -rf`, `… | xargs -0 -- rm --recursive
|
|
1052
|
+
# --force`, `… 2>&1 | parallel rm -rf`, `… |& parallel rm -rf` and
|
|
1053
|
+
# `cat .git/config | grep url | rm -rf refs` were ALL allowed (CodySwannGT/lisa#3320).
|
|
1054
|
+
#
|
|
1055
|
+
# So segment on STATEMENT boundaries and nothing else. A pipe joins stages of
|
|
1056
|
+
# one statement; `;`, `&`, `&&` and `||` end one. Three spellings have to be
|
|
1057
|
+
# normalized first or a naive split gets them wrong in both directions:
|
|
1058
|
+
#
|
|
1059
|
+
# `|&` bash's pipe-with-stderr. Splitting its `&` would halve a pipeline —
|
|
1060
|
+
# the same fail-open, wearing a different operator. Becomes `|`.
|
|
1061
|
+
# `2>&1` a redirection, not a boundary (also `>&2`, `<&3`, `&>file`). These
|
|
1062
|
+
# are where `&` appears WITHOUT ending a statement, and they are
|
|
1063
|
+
# common enough that ignoring them would reopen the hole on any
|
|
1064
|
+
# pipeline that redirects stderr. The `&` is folded away.
|
|
1065
|
+
# `||` a real boundary that happens to be built from pipe characters, so it
|
|
1066
|
+
# must be recognized BEFORE a single `|` is spared. Becomes `;`.
|
|
1067
|
+
#
|
|
1068
|
+
# Every one of those rewrites only ever REMOVES a boundary or leaves it intact,
|
|
1069
|
+
# so each can merge two segments but never split one — the fail-closed
|
|
1070
|
+
# direction. `&` and `;` still separate, so unrelated commands are not joined.
|
|
1071
|
+
#
|
|
1072
|
+
# The leading awk is the same idea one level up. A line ending in `|` is bash's
|
|
1073
|
+
# OTHER implicit continuation — the pipeline resumes on the next line — and the
|
|
1074
|
+
# per-line read below would otherwise part the stages exactly as `tr` did, so
|
|
1075
|
+
# `find .git -type d |`↵` parallel rm -rf` fails open through the newline
|
|
1076
|
+
# instead of through the pipe. This mirrors the backslash-continuation join that
|
|
1077
|
+
# `normalized_command_str` already performs for the same reason. It is done here
|
|
1078
|
+
# rather than in that shared normalization deliberately: this guard is the one
|
|
1079
|
+
# that pairs facts across stages, so it is the one that needs the stages kept
|
|
1080
|
+
# together, and widening the shared normalization would change every other
|
|
1081
|
+
# guard's segmentation for no defect anyone has measured.
|
|
1082
|
+
#
|
|
1083
|
+
# The status check is the second half of the fix and closes a quieter hole. This
|
|
1084
|
+
# feeder had none, while the rm and push walks above both have one: a segmenter
|
|
1085
|
+
# that could not run produced no lines, the loop body never executed, and the
|
|
1086
|
+
# guard was skipped with the command ALLOWED. A segmentation that cannot answer
|
|
1087
|
+
# must refuse, exactly like a scan that cannot answer.
|
|
1088
|
+
git_segments_status=0
|
|
1089
|
+
git_segments="$(printf '%s' "$normalized_command_str" \
|
|
1090
|
+
| awk '{ if (/\|[ \t]*$/) printf "%s ", $0; else print }' \
|
|
1091
|
+
| sed -e 's/|&/|/g' \
|
|
1092
|
+
-e 's/&>/>/g' \
|
|
1093
|
+
-e 's/[0-9]*>&[0-9-]*/>/g' \
|
|
1094
|
+
-e 's/[0-9]*<&[0-9-]*/</g' \
|
|
1095
|
+
-e 's/||/;/g' \
|
|
1096
|
+
| tr '&;' '\n')" || git_segments_status=$?
|
|
1097
|
+
case "$git_segments_status" in
|
|
1098
|
+
0) ;;
|
|
1099
|
+
*) scan_failed "$git_segments_status" ;;
|
|
1100
|
+
esac
|
|
1038
1101
|
while IFS= read -r git_stmt; do
|
|
1039
1102
|
[ -n "$git_stmt" ] || continue
|
|
1040
1103
|
if ! scan -E "$git_stmt" "$RM_RF_CLUSTER" \
|
|
@@ -1044,7 +1107,7 @@ while IFS= read -r git_stmt; do
|
|
|
1044
1107
|
if scan -E "$git_stmt" "$GIT_CONTROL_PLANE"; then
|
|
1045
1108
|
block "recursive forced delete of the git control plane (.git holds every commit, branch and stash not already pushed; nothing in the working tree can rebuild it). Delete a specific ignored artifact instead, or re-clone if the checkout is genuinely to be discarded."
|
|
1046
1109
|
fi
|
|
1047
|
-
done <<<
|
|
1110
|
+
done <<<"$git_segments"
|
|
1048
1111
|
|
|
1049
1112
|
# 15. Credential stores. Reading one into an agent transcript is disclosure even
|
|
1050
1113
|
# when nothing is copied anywhere: the value lands in a log, a context window,
|
|
@@ -15,12 +15,27 @@ name: 🌙 Nightly E2E Report
|
|
|
15
15
|
#
|
|
16
16
|
# ## This is NOT a gate — never make it a required check
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
18
|
+
# Requiring this context would hand an issues-API outage the power to block every
|
|
19
|
+
# pull request, which is exactly the coupling the two-workflow split exists to
|
|
20
|
+
# prevent: the gate workflow does not request `issues:` scope at all, so it
|
|
21
|
+
# cannot file, and this one is not required, so it cannot block.
|
|
22
|
+
#
|
|
23
|
+
# ## Publishing is BEST EFFORT — this job's result tracks the SUITE
|
|
24
|
+
#
|
|
25
|
+
# The verdict goes to `$GITHUB_STEP_SUMMARY` first, then the tracking issue is
|
|
26
|
+
# attempted, then whatever publishing did becomes a `::warning::`. So:
|
|
27
|
+
#
|
|
28
|
+
# * A tracking issue that could not be written — Issues disabled on the
|
|
29
|
+
# repository, an API outage, a revoked scope — is a WARNING, and the verdict
|
|
30
|
+
# is still on the run's summary. The report channel does not die because its
|
|
31
|
+
# optional publishing destination is unavailable.
|
|
32
|
+
# * A genuinely RED suite fails this job.
|
|
33
|
+
# * A verdict that could not be resolved at all fails this job.
|
|
34
|
+
#
|
|
35
|
+
# Do NOT reach for `continue-on-error:` on the job below to quieten a failing
|
|
36
|
+
# publish. It greens every run, including the ones that genuinely failed to
|
|
37
|
+
# report — which is the failure this design exists to avoid. If publishing is
|
|
38
|
+
# failing, the warning names the cause; fix that.
|
|
24
39
|
#
|
|
25
40
|
# ## Wiring this up
|
|
26
41
|
#
|
package/package.json
CHANGED
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"zod-validation-error": "^4.0.0"
|
|
162
162
|
},
|
|
163
163
|
"name": "@codyswann/lisa",
|
|
164
|
-
"version": "4.26.
|
|
164
|
+
"version": "4.26.2",
|
|
165
165
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
166
166
|
"main": "dist/index.js",
|
|
167
167
|
"exports": {
|
|
@@ -305,6 +305,6 @@
|
|
|
305
305
|
"test": "tests"
|
|
306
306
|
},
|
|
307
307
|
"types": "./dist/index.d.ts",
|
|
308
|
-
"lisaReleaseCommit": "
|
|
309
|
-
"gitHead": "
|
|
308
|
+
"lisaReleaseCommit": "671183a45139c27c367599e7c7adb963750dfb19",
|
|
309
|
+
"gitHead": "671183a45139c27c367599e7c7adb963750dfb19"
|
|
310
310
|
}
|
|
@@ -1035,6 +1035,69 @@ fi
|
|
|
1035
1035
|
# purpose, and blocking those would be the collision that gets a guard switched
|
|
1036
1036
|
# off. The trailing `([/[:space:]'"$qc"']|$)` is what draws that line.
|
|
1037
1037
|
readonly GIT_CONTROL_PLANE='(^|[[:space:]='"$qc"'./])\.git([/[:space:]'"$qc"']|$)'
|
|
1038
|
+
# This guard is the one segment walk that pairs TWO facts found in different
|
|
1039
|
+
# places: the protected target (`.git`) and the destructive verb (a recursive
|
|
1040
|
+
# forced delete). That makes its segmentation load-bearing in a way the rm and
|
|
1041
|
+
# push walks' is not — those two read a verb and its own operands, which are
|
|
1042
|
+
# always in one pipeline stage, so splitting on `|` costs them nothing.
|
|
1043
|
+
#
|
|
1044
|
+
# Here it cost everything. The old feeder was `tr '&|;' '\n'`, which cut a
|
|
1045
|
+
# PIPELINE into pieces before the pairing question was asked, and a pipeline is
|
|
1046
|
+
# exactly how a delete reaches a target named upstream of it:
|
|
1047
|
+
# `find .git -type f -print0 | xargs -0 rm -rf` became `find .git -type f
|
|
1048
|
+
# -print0` (a target, no verb) and `xargs -0 rm -rf` (a verb, no target), and
|
|
1049
|
+
# neither half could answer. Measured before this fix, with the blanket
|
|
1050
|
+
# `xargs rm -rf` rule masking the reported shape: `find .git -type d | parallel
|
|
1051
|
+
# rm -rf`, `… | /usr/bin/xargs rm -rf`, `… | xargs -0 -- rm --recursive
|
|
1052
|
+
# --force`, `… 2>&1 | parallel rm -rf`, `… |& parallel rm -rf` and
|
|
1053
|
+
# `cat .git/config | grep url | rm -rf refs` were ALL allowed (CodySwannGT/lisa#3320).
|
|
1054
|
+
#
|
|
1055
|
+
# So segment on STATEMENT boundaries and nothing else. A pipe joins stages of
|
|
1056
|
+
# one statement; `;`, `&`, `&&` and `||` end one. Three spellings have to be
|
|
1057
|
+
# normalized first or a naive split gets them wrong in both directions:
|
|
1058
|
+
#
|
|
1059
|
+
# `|&` bash's pipe-with-stderr. Splitting its `&` would halve a pipeline —
|
|
1060
|
+
# the same fail-open, wearing a different operator. Becomes `|`.
|
|
1061
|
+
# `2>&1` a redirection, not a boundary (also `>&2`, `<&3`, `&>file`). These
|
|
1062
|
+
# are where `&` appears WITHOUT ending a statement, and they are
|
|
1063
|
+
# common enough that ignoring them would reopen the hole on any
|
|
1064
|
+
# pipeline that redirects stderr. The `&` is folded away.
|
|
1065
|
+
# `||` a real boundary that happens to be built from pipe characters, so it
|
|
1066
|
+
# must be recognized BEFORE a single `|` is spared. Becomes `;`.
|
|
1067
|
+
#
|
|
1068
|
+
# Every one of those rewrites only ever REMOVES a boundary or leaves it intact,
|
|
1069
|
+
# so each can merge two segments but never split one — the fail-closed
|
|
1070
|
+
# direction. `&` and `;` still separate, so unrelated commands are not joined.
|
|
1071
|
+
#
|
|
1072
|
+
# The leading awk is the same idea one level up. A line ending in `|` is bash's
|
|
1073
|
+
# OTHER implicit continuation — the pipeline resumes on the next line — and the
|
|
1074
|
+
# per-line read below would otherwise part the stages exactly as `tr` did, so
|
|
1075
|
+
# `find .git -type d |`↵` parallel rm -rf` fails open through the newline
|
|
1076
|
+
# instead of through the pipe. This mirrors the backslash-continuation join that
|
|
1077
|
+
# `normalized_command_str` already performs for the same reason. It is done here
|
|
1078
|
+
# rather than in that shared normalization deliberately: this guard is the one
|
|
1079
|
+
# that pairs facts across stages, so it is the one that needs the stages kept
|
|
1080
|
+
# together, and widening the shared normalization would change every other
|
|
1081
|
+
# guard's segmentation for no defect anyone has measured.
|
|
1082
|
+
#
|
|
1083
|
+
# The status check is the second half of the fix and closes a quieter hole. This
|
|
1084
|
+
# feeder had none, while the rm and push walks above both have one: a segmenter
|
|
1085
|
+
# that could not run produced no lines, the loop body never executed, and the
|
|
1086
|
+
# guard was skipped with the command ALLOWED. A segmentation that cannot answer
|
|
1087
|
+
# must refuse, exactly like a scan that cannot answer.
|
|
1088
|
+
git_segments_status=0
|
|
1089
|
+
git_segments="$(printf '%s' "$normalized_command_str" \
|
|
1090
|
+
| awk '{ if (/\|[ \t]*$/) printf "%s ", $0; else print }' \
|
|
1091
|
+
| sed -e 's/|&/|/g' \
|
|
1092
|
+
-e 's/&>/>/g' \
|
|
1093
|
+
-e 's/[0-9]*>&[0-9-]*/>/g' \
|
|
1094
|
+
-e 's/[0-9]*<&[0-9-]*/</g' \
|
|
1095
|
+
-e 's/||/;/g' \
|
|
1096
|
+
| tr '&;' '\n')" || git_segments_status=$?
|
|
1097
|
+
case "$git_segments_status" in
|
|
1098
|
+
0) ;;
|
|
1099
|
+
*) scan_failed "$git_segments_status" ;;
|
|
1100
|
+
esac
|
|
1038
1101
|
while IFS= read -r git_stmt; do
|
|
1039
1102
|
[ -n "$git_stmt" ] || continue
|
|
1040
1103
|
if ! scan -E "$git_stmt" "$RM_RF_CLUSTER" \
|
|
@@ -1044,7 +1107,7 @@ while IFS= read -r git_stmt; do
|
|
|
1044
1107
|
if scan -E "$git_stmt" "$GIT_CONTROL_PLANE"; then
|
|
1045
1108
|
block "recursive forced delete of the git control plane (.git holds every commit, branch and stash not already pushed; nothing in the working tree can rebuild it). Delete a specific ignored artifact instead, or re-clone if the checkout is genuinely to be discarded."
|
|
1046
1109
|
fi
|
|
1047
|
-
done <<<
|
|
1110
|
+
done <<<"$git_segments"
|
|
1048
1111
|
|
|
1049
1112
|
# 15. Credential stores. Reading one into an agent transcript is disclosure even
|
|
1050
1113
|
# when nothing is copied anywhere: the value lands in a log, a context window,
|
|
@@ -1035,6 +1035,69 @@ fi
|
|
|
1035
1035
|
# purpose, and blocking those would be the collision that gets a guard switched
|
|
1036
1036
|
# off. The trailing `([/[:space:]'"$qc"']|$)` is what draws that line.
|
|
1037
1037
|
readonly GIT_CONTROL_PLANE='(^|[[:space:]='"$qc"'./])\.git([/[:space:]'"$qc"']|$)'
|
|
1038
|
+
# This guard is the one segment walk that pairs TWO facts found in different
|
|
1039
|
+
# places: the protected target (`.git`) and the destructive verb (a recursive
|
|
1040
|
+
# forced delete). That makes its segmentation load-bearing in a way the rm and
|
|
1041
|
+
# push walks' is not — those two read a verb and its own operands, which are
|
|
1042
|
+
# always in one pipeline stage, so splitting on `|` costs them nothing.
|
|
1043
|
+
#
|
|
1044
|
+
# Here it cost everything. The old feeder was `tr '&|;' '\n'`, which cut a
|
|
1045
|
+
# PIPELINE into pieces before the pairing question was asked, and a pipeline is
|
|
1046
|
+
# exactly how a delete reaches a target named upstream of it:
|
|
1047
|
+
# `find .git -type f -print0 | xargs -0 rm -rf` became `find .git -type f
|
|
1048
|
+
# -print0` (a target, no verb) and `xargs -0 rm -rf` (a verb, no target), and
|
|
1049
|
+
# neither half could answer. Measured before this fix, with the blanket
|
|
1050
|
+
# `xargs rm -rf` rule masking the reported shape: `find .git -type d | parallel
|
|
1051
|
+
# rm -rf`, `… | /usr/bin/xargs rm -rf`, `… | xargs -0 -- rm --recursive
|
|
1052
|
+
# --force`, `… 2>&1 | parallel rm -rf`, `… |& parallel rm -rf` and
|
|
1053
|
+
# `cat .git/config | grep url | rm -rf refs` were ALL allowed (CodySwannGT/lisa#3320).
|
|
1054
|
+
#
|
|
1055
|
+
# So segment on STATEMENT boundaries and nothing else. A pipe joins stages of
|
|
1056
|
+
# one statement; `;`, `&`, `&&` and `||` end one. Three spellings have to be
|
|
1057
|
+
# normalized first or a naive split gets them wrong in both directions:
|
|
1058
|
+
#
|
|
1059
|
+
# `|&` bash's pipe-with-stderr. Splitting its `&` would halve a pipeline —
|
|
1060
|
+
# the same fail-open, wearing a different operator. Becomes `|`.
|
|
1061
|
+
# `2>&1` a redirection, not a boundary (also `>&2`, `<&3`, `&>file`). These
|
|
1062
|
+
# are where `&` appears WITHOUT ending a statement, and they are
|
|
1063
|
+
# common enough that ignoring them would reopen the hole on any
|
|
1064
|
+
# pipeline that redirects stderr. The `&` is folded away.
|
|
1065
|
+
# `||` a real boundary that happens to be built from pipe characters, so it
|
|
1066
|
+
# must be recognized BEFORE a single `|` is spared. Becomes `;`.
|
|
1067
|
+
#
|
|
1068
|
+
# Every one of those rewrites only ever REMOVES a boundary or leaves it intact,
|
|
1069
|
+
# so each can merge two segments but never split one — the fail-closed
|
|
1070
|
+
# direction. `&` and `;` still separate, so unrelated commands are not joined.
|
|
1071
|
+
#
|
|
1072
|
+
# The leading awk is the same idea one level up. A line ending in `|` is bash's
|
|
1073
|
+
# OTHER implicit continuation — the pipeline resumes on the next line — and the
|
|
1074
|
+
# per-line read below would otherwise part the stages exactly as `tr` did, so
|
|
1075
|
+
# `find .git -type d |`↵` parallel rm -rf` fails open through the newline
|
|
1076
|
+
# instead of through the pipe. This mirrors the backslash-continuation join that
|
|
1077
|
+
# `normalized_command_str` already performs for the same reason. It is done here
|
|
1078
|
+
# rather than in that shared normalization deliberately: this guard is the one
|
|
1079
|
+
# that pairs facts across stages, so it is the one that needs the stages kept
|
|
1080
|
+
# together, and widening the shared normalization would change every other
|
|
1081
|
+
# guard's segmentation for no defect anyone has measured.
|
|
1082
|
+
#
|
|
1083
|
+
# The status check is the second half of the fix and closes a quieter hole. This
|
|
1084
|
+
# feeder had none, while the rm and push walks above both have one: a segmenter
|
|
1085
|
+
# that could not run produced no lines, the loop body never executed, and the
|
|
1086
|
+
# guard was skipped with the command ALLOWED. A segmentation that cannot answer
|
|
1087
|
+
# must refuse, exactly like a scan that cannot answer.
|
|
1088
|
+
git_segments_status=0
|
|
1089
|
+
git_segments="$(printf '%s' "$normalized_command_str" \
|
|
1090
|
+
| awk '{ if (/\|[ \t]*$/) printf "%s ", $0; else print }' \
|
|
1091
|
+
| sed -e 's/|&/|/g' \
|
|
1092
|
+
-e 's/&>/>/g' \
|
|
1093
|
+
-e 's/[0-9]*>&[0-9-]*/>/g' \
|
|
1094
|
+
-e 's/[0-9]*<&[0-9-]*/</g' \
|
|
1095
|
+
-e 's/||/;/g' \
|
|
1096
|
+
| tr '&;' '\n')" || git_segments_status=$?
|
|
1097
|
+
case "$git_segments_status" in
|
|
1098
|
+
0) ;;
|
|
1099
|
+
*) scan_failed "$git_segments_status" ;;
|
|
1100
|
+
esac
|
|
1038
1101
|
while IFS= read -r git_stmt; do
|
|
1039
1102
|
[ -n "$git_stmt" ] || continue
|
|
1040
1103
|
if ! scan -E "$git_stmt" "$RM_RF_CLUSTER" \
|
|
@@ -1044,7 +1107,7 @@ while IFS= read -r git_stmt; do
|
|
|
1044
1107
|
if scan -E "$git_stmt" "$GIT_CONTROL_PLANE"; then
|
|
1045
1108
|
block "recursive forced delete of the git control plane (.git holds every commit, branch and stash not already pushed; nothing in the working tree can rebuild it). Delete a specific ignored artifact instead, or re-clone if the checkout is genuinely to be discarded."
|
|
1046
1109
|
fi
|
|
1047
|
-
done <<<
|
|
1110
|
+
done <<<"$git_segments"
|
|
1048
1111
|
|
|
1049
1112
|
# 15. Credential stores. Reading one into an agent transcript is disclosure even
|
|
1050
1113
|
# when nothing is copied anywhere: the value lands in a log, a context window,
|
|
@@ -1035,6 +1035,69 @@ fi
|
|
|
1035
1035
|
# purpose, and blocking those would be the collision that gets a guard switched
|
|
1036
1036
|
# off. The trailing `([/[:space:]'"$qc"']|$)` is what draws that line.
|
|
1037
1037
|
readonly GIT_CONTROL_PLANE='(^|[[:space:]='"$qc"'./])\.git([/[:space:]'"$qc"']|$)'
|
|
1038
|
+
# This guard is the one segment walk that pairs TWO facts found in different
|
|
1039
|
+
# places: the protected target (`.git`) and the destructive verb (a recursive
|
|
1040
|
+
# forced delete). That makes its segmentation load-bearing in a way the rm and
|
|
1041
|
+
# push walks' is not — those two read a verb and its own operands, which are
|
|
1042
|
+
# always in one pipeline stage, so splitting on `|` costs them nothing.
|
|
1043
|
+
#
|
|
1044
|
+
# Here it cost everything. The old feeder was `tr '&|;' '\n'`, which cut a
|
|
1045
|
+
# PIPELINE into pieces before the pairing question was asked, and a pipeline is
|
|
1046
|
+
# exactly how a delete reaches a target named upstream of it:
|
|
1047
|
+
# `find .git -type f -print0 | xargs -0 rm -rf` became `find .git -type f
|
|
1048
|
+
# -print0` (a target, no verb) and `xargs -0 rm -rf` (a verb, no target), and
|
|
1049
|
+
# neither half could answer. Measured before this fix, with the blanket
|
|
1050
|
+
# `xargs rm -rf` rule masking the reported shape: `find .git -type d | parallel
|
|
1051
|
+
# rm -rf`, `… | /usr/bin/xargs rm -rf`, `… | xargs -0 -- rm --recursive
|
|
1052
|
+
# --force`, `… 2>&1 | parallel rm -rf`, `… |& parallel rm -rf` and
|
|
1053
|
+
# `cat .git/config | grep url | rm -rf refs` were ALL allowed (CodySwannGT/lisa#3320).
|
|
1054
|
+
#
|
|
1055
|
+
# So segment on STATEMENT boundaries and nothing else. A pipe joins stages of
|
|
1056
|
+
# one statement; `;`, `&`, `&&` and `||` end one. Three spellings have to be
|
|
1057
|
+
# normalized first or a naive split gets them wrong in both directions:
|
|
1058
|
+
#
|
|
1059
|
+
# `|&` bash's pipe-with-stderr. Splitting its `&` would halve a pipeline —
|
|
1060
|
+
# the same fail-open, wearing a different operator. Becomes `|`.
|
|
1061
|
+
# `2>&1` a redirection, not a boundary (also `>&2`, `<&3`, `&>file`). These
|
|
1062
|
+
# are where `&` appears WITHOUT ending a statement, and they are
|
|
1063
|
+
# common enough that ignoring them would reopen the hole on any
|
|
1064
|
+
# pipeline that redirects stderr. The `&` is folded away.
|
|
1065
|
+
# `||` a real boundary that happens to be built from pipe characters, so it
|
|
1066
|
+
# must be recognized BEFORE a single `|` is spared. Becomes `;`.
|
|
1067
|
+
#
|
|
1068
|
+
# Every one of those rewrites only ever REMOVES a boundary or leaves it intact,
|
|
1069
|
+
# so each can merge two segments but never split one — the fail-closed
|
|
1070
|
+
# direction. `&` and `;` still separate, so unrelated commands are not joined.
|
|
1071
|
+
#
|
|
1072
|
+
# The leading awk is the same idea one level up. A line ending in `|` is bash's
|
|
1073
|
+
# OTHER implicit continuation — the pipeline resumes on the next line — and the
|
|
1074
|
+
# per-line read below would otherwise part the stages exactly as `tr` did, so
|
|
1075
|
+
# `find .git -type d |`↵` parallel rm -rf` fails open through the newline
|
|
1076
|
+
# instead of through the pipe. This mirrors the backslash-continuation join that
|
|
1077
|
+
# `normalized_command_str` already performs for the same reason. It is done here
|
|
1078
|
+
# rather than in that shared normalization deliberately: this guard is the one
|
|
1079
|
+
# that pairs facts across stages, so it is the one that needs the stages kept
|
|
1080
|
+
# together, and widening the shared normalization would change every other
|
|
1081
|
+
# guard's segmentation for no defect anyone has measured.
|
|
1082
|
+
#
|
|
1083
|
+
# The status check is the second half of the fix and closes a quieter hole. This
|
|
1084
|
+
# feeder had none, while the rm and push walks above both have one: a segmenter
|
|
1085
|
+
# that could not run produced no lines, the loop body never executed, and the
|
|
1086
|
+
# guard was skipped with the command ALLOWED. A segmentation that cannot answer
|
|
1087
|
+
# must refuse, exactly like a scan that cannot answer.
|
|
1088
|
+
git_segments_status=0
|
|
1089
|
+
git_segments="$(printf '%s' "$normalized_command_str" \
|
|
1090
|
+
| awk '{ if (/\|[ \t]*$/) printf "%s ", $0; else print }' \
|
|
1091
|
+
| sed -e 's/|&/|/g' \
|
|
1092
|
+
-e 's/&>/>/g' \
|
|
1093
|
+
-e 's/[0-9]*>&[0-9-]*/>/g' \
|
|
1094
|
+
-e 's/[0-9]*<&[0-9-]*/</g' \
|
|
1095
|
+
-e 's/||/;/g' \
|
|
1096
|
+
| tr '&;' '\n')" || git_segments_status=$?
|
|
1097
|
+
case "$git_segments_status" in
|
|
1098
|
+
0) ;;
|
|
1099
|
+
*) scan_failed "$git_segments_status" ;;
|
|
1100
|
+
esac
|
|
1038
1101
|
while IFS= read -r git_stmt; do
|
|
1039
1102
|
[ -n "$git_stmt" ] || continue
|
|
1040
1103
|
if ! scan -E "$git_stmt" "$RM_RF_CLUSTER" \
|
|
@@ -1044,7 +1107,7 @@ while IFS= read -r git_stmt; do
|
|
|
1044
1107
|
if scan -E "$git_stmt" "$GIT_CONTROL_PLANE"; then
|
|
1045
1108
|
block "recursive forced delete of the git control plane (.git holds every commit, branch and stash not already pushed; nothing in the working tree can rebuild it). Delete a specific ignored artifact instead, or re-clone if the checkout is genuinely to be discarded."
|
|
1046
1109
|
fi
|
|
1047
|
-
done <<<
|
|
1110
|
+
done <<<"$git_segments"
|
|
1048
1111
|
|
|
1049
1112
|
# 15. Credential stores. Reading one into an agent transcript is disclosure even
|
|
1050
1113
|
# when nothing is copied anywhere: the value lands in a log, a context window,
|
|
@@ -1035,6 +1035,69 @@ fi
|
|
|
1035
1035
|
# purpose, and blocking those would be the collision that gets a guard switched
|
|
1036
1036
|
# off. The trailing `([/[:space:]'"$qc"']|$)` is what draws that line.
|
|
1037
1037
|
readonly GIT_CONTROL_PLANE='(^|[[:space:]='"$qc"'./])\.git([/[:space:]'"$qc"']|$)'
|
|
1038
|
+
# This guard is the one segment walk that pairs TWO facts found in different
|
|
1039
|
+
# places: the protected target (`.git`) and the destructive verb (a recursive
|
|
1040
|
+
# forced delete). That makes its segmentation load-bearing in a way the rm and
|
|
1041
|
+
# push walks' is not — those two read a verb and its own operands, which are
|
|
1042
|
+
# always in one pipeline stage, so splitting on `|` costs them nothing.
|
|
1043
|
+
#
|
|
1044
|
+
# Here it cost everything. The old feeder was `tr '&|;' '\n'`, which cut a
|
|
1045
|
+
# PIPELINE into pieces before the pairing question was asked, and a pipeline is
|
|
1046
|
+
# exactly how a delete reaches a target named upstream of it:
|
|
1047
|
+
# `find .git -type f -print0 | xargs -0 rm -rf` became `find .git -type f
|
|
1048
|
+
# -print0` (a target, no verb) and `xargs -0 rm -rf` (a verb, no target), and
|
|
1049
|
+
# neither half could answer. Measured before this fix, with the blanket
|
|
1050
|
+
# `xargs rm -rf` rule masking the reported shape: `find .git -type d | parallel
|
|
1051
|
+
# rm -rf`, `… | /usr/bin/xargs rm -rf`, `… | xargs -0 -- rm --recursive
|
|
1052
|
+
# --force`, `… 2>&1 | parallel rm -rf`, `… |& parallel rm -rf` and
|
|
1053
|
+
# `cat .git/config | grep url | rm -rf refs` were ALL allowed (CodySwannGT/lisa#3320).
|
|
1054
|
+
#
|
|
1055
|
+
# So segment on STATEMENT boundaries and nothing else. A pipe joins stages of
|
|
1056
|
+
# one statement; `;`, `&`, `&&` and `||` end one. Three spellings have to be
|
|
1057
|
+
# normalized first or a naive split gets them wrong in both directions:
|
|
1058
|
+
#
|
|
1059
|
+
# `|&` bash's pipe-with-stderr. Splitting its `&` would halve a pipeline —
|
|
1060
|
+
# the same fail-open, wearing a different operator. Becomes `|`.
|
|
1061
|
+
# `2>&1` a redirection, not a boundary (also `>&2`, `<&3`, `&>file`). These
|
|
1062
|
+
# are where `&` appears WITHOUT ending a statement, and they are
|
|
1063
|
+
# common enough that ignoring them would reopen the hole on any
|
|
1064
|
+
# pipeline that redirects stderr. The `&` is folded away.
|
|
1065
|
+
# `||` a real boundary that happens to be built from pipe characters, so it
|
|
1066
|
+
# must be recognized BEFORE a single `|` is spared. Becomes `;`.
|
|
1067
|
+
#
|
|
1068
|
+
# Every one of those rewrites only ever REMOVES a boundary or leaves it intact,
|
|
1069
|
+
# so each can merge two segments but never split one — the fail-closed
|
|
1070
|
+
# direction. `&` and `;` still separate, so unrelated commands are not joined.
|
|
1071
|
+
#
|
|
1072
|
+
# The leading awk is the same idea one level up. A line ending in `|` is bash's
|
|
1073
|
+
# OTHER implicit continuation — the pipeline resumes on the next line — and the
|
|
1074
|
+
# per-line read below would otherwise part the stages exactly as `tr` did, so
|
|
1075
|
+
# `find .git -type d |`↵` parallel rm -rf` fails open through the newline
|
|
1076
|
+
# instead of through the pipe. This mirrors the backslash-continuation join that
|
|
1077
|
+
# `normalized_command_str` already performs for the same reason. It is done here
|
|
1078
|
+
# rather than in that shared normalization deliberately: this guard is the one
|
|
1079
|
+
# that pairs facts across stages, so it is the one that needs the stages kept
|
|
1080
|
+
# together, and widening the shared normalization would change every other
|
|
1081
|
+
# guard's segmentation for no defect anyone has measured.
|
|
1082
|
+
#
|
|
1083
|
+
# The status check is the second half of the fix and closes a quieter hole. This
|
|
1084
|
+
# feeder had none, while the rm and push walks above both have one: a segmenter
|
|
1085
|
+
# that could not run produced no lines, the loop body never executed, and the
|
|
1086
|
+
# guard was skipped with the command ALLOWED. A segmentation that cannot answer
|
|
1087
|
+
# must refuse, exactly like a scan that cannot answer.
|
|
1088
|
+
git_segments_status=0
|
|
1089
|
+
git_segments="$(printf '%s' "$normalized_command_str" \
|
|
1090
|
+
| awk '{ if (/\|[ \t]*$/) printf "%s ", $0; else print }' \
|
|
1091
|
+
| sed -e 's/|&/|/g' \
|
|
1092
|
+
-e 's/&>/>/g' \
|
|
1093
|
+
-e 's/[0-9]*>&[0-9-]*/>/g' \
|
|
1094
|
+
-e 's/[0-9]*<&[0-9-]*/</g' \
|
|
1095
|
+
-e 's/||/;/g' \
|
|
1096
|
+
| tr '&;' '\n')" || git_segments_status=$?
|
|
1097
|
+
case "$git_segments_status" in
|
|
1098
|
+
0) ;;
|
|
1099
|
+
*) scan_failed "$git_segments_status" ;;
|
|
1100
|
+
esac
|
|
1038
1101
|
while IFS= read -r git_stmt; do
|
|
1039
1102
|
[ -n "$git_stmt" ] || continue
|
|
1040
1103
|
if ! scan -E "$git_stmt" "$RM_RF_CLUSTER" \
|
|
@@ -1044,7 +1107,7 @@ while IFS= read -r git_stmt; do
|
|
|
1044
1107
|
if scan -E "$git_stmt" "$GIT_CONTROL_PLANE"; then
|
|
1045
1108
|
block "recursive forced delete of the git control plane (.git holds every commit, branch and stash not already pushed; nothing in the working tree can rebuild it). Delete a specific ignored artifact instead, or re-clone if the checkout is genuinely to be discarded."
|
|
1046
1109
|
fi
|
|
1047
|
-
done <<<
|
|
1110
|
+
done <<<"$git_segments"
|
|
1048
1111
|
|
|
1049
1112
|
# 15. Credential stores. Reading one into an agent transcript is disclosure even
|
|
1050
1113
|
# when nothing is copied anywhere: the value lands in a log, a context window,
|