|
| 1 | +""" |
| 2 | +Location: pact-plugin/tests/test_merge_guard_1037_narrow.py |
| 3 | +Summary: Regression canaries for the NARROW gh-comment carrier-strip (#1037) — the |
| 4 | + doubly-anchored step-7 `_gh_carrier_span` extension that adds |
| 5 | + `gh issue comment` + `gh pr comment` to the carrier verb alternation. |
| 6 | + Locks in: gh-comment --title/--body/-t/-b prose that NAMES a dangerous op |
| 7 | + now ALLOWs; every executing-op vector still BLOCKs; create/edit unchanged. |
| 8 | + The general HYBRID suppressor approach was abandoned (a single-line regex |
| 9 | + cannot model bash grammar — it shipped under-blocks); this narrow |
| 10 | + carrier-strip is the safe partial fix for the gh-comment over-block. |
| 11 | +Used by: pytest (merge-guard suite). |
| 12 | +
|
| 13 | +Non-vacuity: |
| 14 | + * ALLOW canaries (proof-class P): the comment-verb membership is what flips them. |
| 15 | + Proven dynamically by VERB-DISCRIMINATION (a carrier verb ALLOWs an inert |
| 16 | + --body; the SAME --body under a non-carrier sibling verb `gh pr edit` BLOCKs) |
| 17 | + + a mechanism assertion (_strip blanks the --body value to STRIPPED, so the |
| 18 | + dangerous literal never reaches DANGEROUS_PATTERNS). Build-time source-revert |
| 19 | + MEASUREMENT (remove the comment verbs from `_gh_carrier_span`): {5 of 5 ALLOW |
| 20 | + flip to BLOCK}. `_gh_carrier_span` is FUNCTION-LOCAL to |
| 21 | + _strip_non_executable_content, so this is a measured/documented counter-test |
| 22 | + (the discrimination is the runnable equivalent), not an in-test monkeypatch. |
| 23 | + * BLOCK canaries (proof-class C): a broaden/neuter flips a preserved-block to |
| 24 | + ALLOW, OR (span-stop cases) a discrimination shows the op moves outside the |
| 25 | + stripped value — see each test's inline note for the EXACT proven mechanism. |
| 26 | +
|
| 27 | +Dangerous literals are authored IN-FILE only (never on a Bash command line — the |
| 28 | +installed merge-guard hook false-positives on them). Backslash and single-quote |
| 29 | +are built from char vars so the escaped-quote vectors are unambiguous. |
| 30 | +""" |
| 31 | +import sys |
| 32 | +from pathlib import Path |
| 33 | + |
| 34 | +sys.path.insert(0, str(Path(__file__).parent.parent / "hooks")) |
| 35 | + |
| 36 | +import pytest |
| 37 | + |
| 38 | +_BS = "\\" # one literal backslash |
| 39 | +_SQ = "'" |
| 40 | + |
| 41 | + |
| 42 | +class TestNarrowGhCommentCarrierStripAllow: |
| 43 | + """The fix: `gh issue|pr comment --body/-b` prose that NAMES a dangerous op is |
| 44 | + now ALLOWED (was a false-positive BLOCK). NEGATIVE assertions — non-vacuity via |
| 45 | + the verb-discrimination + mechanism assertion below.""" |
| 46 | + |
| 47 | + ALLOW = [ |
| 48 | + 'gh pr comment 5 --body "see gh pr merge 5 for context"', |
| 49 | + 'gh issue comment 12 --body "do not git push --force origin main"', |
| 50 | + 'gh pr comment 5 -b "ship after gh pr merge 9"', |
| 51 | + "gh issue comment 7 --body 'note: gh pr merge 3 later'", # single-quoted body |
| 52 | + ] |
| 53 | + |
| 54 | + @pytest.mark.parametrize("command", ALLOW) |
| 55 | + def test_comment_body_naming_dangerous_op_now_allows(self, command): |
| 56 | + from merge_guard_pre import ( |
| 57 | + is_dangerous_command, |
| 58 | + is_compound_destructive_command, |
| 59 | + ) |
| 60 | + |
| 61 | + assert not is_dangerous_command(command) |
| 62 | + assert not is_compound_destructive_command(command) |
| 63 | + |
| 64 | + def test_carrier_strip_neutralizes_the_body(self): |
| 65 | + # mechanism (exclusion-guard, #933): the comment carrier-strip BLANKS the |
| 66 | + # --body value to STRIPPED, so the dangerous literal never reaches the scan. |
| 67 | + from merge_guard_pre import _strip_non_executable_content |
| 68 | + |
| 69 | + stripped = _strip_non_executable_content('gh pr comment 5 --body "gh pr merge 5"') |
| 70 | + assert "gh pr merge" not in stripped |
| 71 | + assert "STRIPPED" in stripped |
| 72 | + assert stripped.startswith("gh pr comment 5") |
| 73 | + |
| 74 | + @pytest.mark.parametrize( |
| 75 | + "carrier,sibling", |
| 76 | + [ |
| 77 | + ( |
| 78 | + 'gh pr comment 5 --body "ref gh pr merge 5"', |
| 79 | + 'gh pr edit 5 --body "ref gh pr merge 5"', |
| 80 | + ), |
| 81 | + ( |
| 82 | + 'gh pr comment 8 --body "ref git push --force origin main"', |
| 83 | + 'gh pr edit 8 --body "ref git push --force origin main"', |
| 84 | + ), |
| 85 | + ], |
| 86 | + ) |
| 87 | + def test_comment_verb_membership_is_load_bearing(self, carrier, sibling): |
| 88 | + # non-vacuity (proof-class P): the comment verb being IN the alternation is |
| 89 | + # what flips these. The identical --body under a NON-carrier sibling verb |
| 90 | + # (`gh pr edit`, absent from the pr alternation) is NOT stripped and BLOCKs. |
| 91 | + # This is the runnable equivalent of reverting the comment verbs from |
| 92 | + # `_gh_carrier_span` (build-time measured: {5 of 5 ALLOW flip to BLOCK}). |
| 93 | + from merge_guard_pre import is_dangerous_command |
| 94 | + |
| 95 | + assert not is_dangerous_command(carrier) # carrier verb -> stripped -> ALLOW |
| 96 | + assert is_dangerous_command(sibling) # non-carrier verb -> survives -> BLOCK |
| 97 | + |
| 98 | + |
| 99 | +class TestNarrowGhCommentCarrierStripBlock: |
| 100 | + """No under-block: every executing-op vector still BLOCKs. The carrier-strip is |
| 101 | + DOUBLY-ANCHORED (carrier verb + value DIRECTLY after --title/--body/-t/-b) and |
| 102 | + the span stops at the first UNQUOTED separator, so an op outside the carrier |
| 103 | + value always survives into DANGEROUS_PATTERNS (the authority).""" |
| 104 | + |
| 105 | + def test_op_after_body_unquoted_separator_blocks(self): |
| 106 | + # non-vacuity (proof-class C, span-stop discrimination): the op INSIDE the |
| 107 | + # body ALLOWs; the SAME op after an UNQUOTED `;` BLOCKs (the span stops at |
| 108 | + # the separator, the op falls OUTSIDE the stripped value and survives). |
| 109 | + from merge_guard_pre import ( |
| 110 | + is_dangerous_command, |
| 111 | + is_compound_destructive_command, |
| 112 | + _strip_non_executable_content, |
| 113 | + ) |
| 114 | + |
| 115 | + inside = 'gh pr comment 5 --body "x gh pr merge 5"' |
| 116 | + outside = 'gh pr comment 5 --body "x" ; gh pr merge 5' |
| 117 | + assert not is_dangerous_command(inside) # op inside the body -> ALLOW |
| 118 | + assert is_compound_destructive_command(outside) # op after `;` -> BLOCK |
| 119 | + assert "gh pr merge 5" in _strip_non_executable_content(outside) # op survives |
| 120 | + |
| 121 | + def test_op_after_body_andand_blocks(self): |
| 122 | + from merge_guard_pre import is_compound_destructive_command |
| 123 | + |
| 124 | + assert is_compound_destructive_command('gh pr comment 5 --body "x" && gh pr merge 9') |
| 125 | + |
| 126 | + def test_command_substitution_in_body_blocks(self, monkeypatch): |
| 127 | + # non-vacuity (proof-class C): the dq inner-strip PRESERVES a $()/backtick |
| 128 | + # body via `_has_command_substitution` (it executes inside double quotes). |
| 129 | + # Neuter that guard -> the body is blanked -> ALLOW (MEASURED flip), proving |
| 130 | + # the preserve guard is the load-bearing reason this stays BLOCK. |
| 131 | + from merge_guard_pre import is_dangerous_command |
| 132 | + |
| 133 | + cmd = 'gh pr comment 5 --body "$(gh pr merge 5)"' |
| 134 | + assert is_dangerous_command(cmd) |
| 135 | + monkeypatch.setattr("merge_guard_pre._has_command_substitution", lambda q: False) |
| 136 | + assert not is_dangerous_command(cmd) # flips ALLOW under the neuter |
| 137 | + |
| 138 | + def test_escaped_quote_outside_carrier_flag_blocks(self): |
| 139 | + # an escaped quote NOT after a carrier flag does not open a carrier-flag |
| 140 | + # value, so the op after the (real, unquoted) separator survives the strip. |
| 141 | + # non-vacuity (mechanism): the op literal is present in the _strip output. |
| 142 | + from merge_guard_pre import is_dangerous_command, _strip_non_executable_content |
| 143 | + |
| 144 | + cmd = "gh pr comment 5 --foo a" + _BS + _SQ + " ; gh pr merge 5 " + _BS + _SQ + "b" |
| 145 | + assert is_dangerous_command(cmd) |
| 146 | + assert "gh pr merge 5" in _strip_non_executable_content(cmd) |
| 147 | + |
| 148 | + def test_close_delete_branch_blocks(self): |
| 149 | + # `gh pr close` is NOT a carrier verb (absent from the alternation BY |
| 150 | + # CONSTRUCTION); `--delete-branch` is the deny trigger -> BLOCK. |
| 151 | + from merge_guard_pre import is_dangerous_command |
| 152 | + |
| 153 | + assert is_dangerous_command("gh pr close 5 --delete-branch") |
| 154 | + |
| 155 | + def test_python_dash_c_blocks(self): |
| 156 | + # python -c EXECUTES its arg; not a carrier verb -> not stripped -> BLOCK. |
| 157 | + # `print('...')` style per the security ratification (an `x='...'` form would |
| 158 | + # be removed by the pre-existing var-assignment carrier and is a weaker canary). |
| 159 | + from merge_guard_pre import is_dangerous_command |
| 160 | + |
| 161 | + assert is_dangerous_command('python3 -c "print(' + _SQ + "gh pr merge 42" + _SQ + ')"') |
| 162 | + |
| 163 | + def test_general_grep_papercut_still_blocks(self): |
| 164 | + # SUPPRESSOR-NEUTRAL regression guard: a bare `grep "...op..."` is a known |
| 165 | + # #1037 over-block papercut the NARROW fix deliberately does NOT address |
| 166 | + # (out of scope — only gh-comment). The carrier-strip never touches grep (not |
| 167 | + # a gh carrier verb), so NO carrier mutation flips this; it is caught by the |
| 168 | + # base substring scan. Asserted so a future change cannot silently ALLOW grep |
| 169 | + # without the proper fix. (NOTE: `echo "...op..."` is correctly ALLOWED by the |
| 170 | + # pre-existing echo carrier-strip — it is NOT a BLOCK canary.) |
| 171 | + from merge_guard_pre import is_dangerous_command |
| 172 | + |
| 173 | + assert is_dangerous_command('grep "gh pr merge 5" file') |
| 174 | + |
| 175 | + |
| 176 | +class TestNarrowGhCommentInertAllow: |
| 177 | + """Ratified INERT-ALLOW: an escaped quote INSIDE a dq --body value is a literal |
| 178 | + char in bash, so the entire dq is ONE inert --body value (the op is never a shell |
| 179 | + token). ALLOW is CORRECT, NOT an under-block.""" |
| 180 | + |
| 181 | + def test_escaped_quote_inside_body_is_inert_allow(self): |
| 182 | + # `gh pr comment 5 --body "a\' ; gh pr merge 5 \'b"` — the `\'` is literal |
| 183 | + # inside the dq; the whole dq string is the --body API value; the op cannot |
| 184 | + # execute. The carrier-strip blanks the whole inert value -> ALLOW. This is |
| 185 | + # NOT an under-block: in bash the op never becomes a command. |
| 186 | + from merge_guard_pre import is_dangerous_command, _strip_non_executable_content |
| 187 | + |
| 188 | + cmd = ( |
| 189 | + "gh pr comment 5 --body " + '"' + "a" + _BS + _SQ |
| 190 | + + " ; gh pr merge 5 " + _BS + _SQ + "b" + '"' |
| 191 | + ) |
| 192 | + assert not is_dangerous_command(cmd) |
| 193 | + assert "gh pr merge" not in _strip_non_executable_content(cmd) |
| 194 | + |
| 195 | + |
| 196 | +class TestNarrowGhCarrierCreateEditUnchanged: |
| 197 | + """Regression guard: the narrow change only ADDED comment to the alternation; |
| 198 | + create/edit carrier behavior is unchanged.""" |
| 199 | + |
| 200 | + CREATE_EDIT = [ |
| 201 | + 'gh issue create --title "regression: gh pr merge 5 in title"', |
| 202 | + 'gh pr create --body "do not git push --force origin main"', |
| 203 | + 'gh issue edit 4 --body "ref gh pr merge 2"', |
| 204 | + ] |
| 205 | + |
| 206 | + @pytest.mark.parametrize("command", CREATE_EDIT) |
| 207 | + def test_create_edit_carrier_still_allows(self, command): |
| 208 | + from merge_guard_pre import is_dangerous_command |
| 209 | + |
| 210 | + assert not is_dangerous_command(command) |
| 211 | + |
| 212 | + |
| 213 | +class TestNarrowGhCommentSecurityAnchoringCanaries: |
| 214 | + """Security re-probe (#36 SAFE 42-case differential) anchoring canaries directed |
| 215 | + at the COMMENT carrier — the two classes that bit the ABANDONED general |
| 216 | + suppressor and that the narrow's FLAG-ANCHORED inner strip (blanks ONLY the |
| 217 | + value DIRECTLY after --title/--body/-t/-b) neutralizes. Highest-value guards: |
| 218 | + they flip RED if the inner strip were ever loosened from FLAG-ANCHORED to |
| 219 | + WHOLE-SEGMENT (blank any quoted region in the span) — the precise mutation that |
| 220 | + distinguishes the SAFE narrow carrier-strip from the abandoned general |
| 221 | + suppressor (which blanked ALL quoted literals in a segment, so it under-blocked). |
| 222 | +
|
| 223 | + Non-vacuity: each canary asserts BLOCK + the op LITERAL SURVIVES the strip |
| 224 | + (present in _strip output, so DANGEROUS_PATTERNS catches it — proving the |
| 225 | + flag-anchoring did NOT blank it). The distinguishing mutation is MEASURED at |
| 226 | + build time: loosening the inner-strip flag-anchor |
| 227 | + (`(?:--title|--body|-t|-b)\\s+` -> `\\s*`, i.e. blank ANY quoted region) flips |
| 228 | + {4 of 4} flag-anchoring canaries (2 CLASS-1 non-carrier-flag + 2 CLASS-2 |
| 229 | + `<(...)`) BLOCK->ALLOW = under-block, while the `>(...)` output-procsub stays |
| 230 | + BLOCK (defense-in-depth: the whole-command process-sub-to-shell guard skips the |
| 231 | + strip entirely). The inner strip is FUNCTION-LOCAL to |
| 232 | + _strip_non_executable_content, so the loosen is a documented measurement and |
| 233 | + op-survives-the-strip is the runnable equivalent. |
| 234 | + """ |
| 235 | + |
| 236 | + # CLASS-1: a dangerous op in a QUOTED value NOT directly after a carrier flag |
| 237 | + # (flag-anchored strip leaves it; a whole-segment strip would blank it). |
| 238 | + CLASS1_FLAG_ANCHORED = [ |
| 239 | + 'gh pr comment 5 --foo "gh pr merge 5"', |
| 240 | + 'gh pr comment 5 --body "ok" --foo "gh pr merge 9"', |
| 241 | + ] |
| 242 | + # CLASS-2: process-substitution running its OWN command (incl --admin / the |
| 243 | + # #1042 bypass and git push --force); the `<(...)` op survives the strip. |
| 244 | + CLASS2_PROCSUB = [ |
| 245 | + 'gh pr comment 5 --body "x" <(bash -c "gh pr merge 5 --admin")', |
| 246 | + 'gh pr comment 5 --body "x" <(bash -c "git push --force origin main")', |
| 247 | + ] |
| 248 | + |
| 249 | + @pytest.mark.parametrize("command", CLASS1_FLAG_ANCHORED + CLASS2_PROCSUB) |
| 250 | + def test_flag_anchoring_canary_blocks_and_op_survives(self, command): |
| 251 | + # BLOCK + the op SURVIVES the strip (flag-anchoring did NOT blank it). The |
| 252 | + # whole-segment-loosen mutation flips these {4 of 4} -> ALLOW (measured). |
| 253 | + from merge_guard_pre import ( |
| 254 | + is_dangerous_command, |
| 255 | + is_compound_destructive_command, |
| 256 | + _strip_non_executable_content, |
| 257 | + ) |
| 258 | + |
| 259 | + assert is_dangerous_command(command) or is_compound_destructive_command(command) |
| 260 | + stripped = _strip_non_executable_content(command) |
| 261 | + assert ("gh pr merge" in stripped) or ("git push" in stripped) # op survives |
| 262 | + |
| 263 | + def test_escaped_quote_then_real_op_blocks(self): |
| 264 | + # CLASS-1 escaped-quote-then-real-op (span-stop): a `\'` desync ends the sq |
| 265 | + # (bash sq has no escapes), so the op after the real `;` is BARE and OUTSIDE |
| 266 | + # the carrier span -> survives -> BLOCK. (Flips under a different mutation, |
| 267 | + # span-consume-past-separator, not the whole-segment-loosen.) |
| 268 | + from merge_guard_pre import ( |
| 269 | + is_dangerous_command, |
| 270 | + is_compound_destructive_command, |
| 271 | + _strip_non_executable_content, |
| 272 | + ) |
| 273 | + |
| 274 | + cmd = "gh pr comment 5 --body " + _SQ + "a" + _BS + _SQ + " ; gh pr merge 5" |
| 275 | + assert is_dangerous_command(cmd) or is_compound_destructive_command(cmd) |
| 276 | + assert "gh pr merge 5" in _strip_non_executable_content(cmd) |
| 277 | + |
| 278 | + def test_op_after_body_with_admin_blocks(self): |
| 279 | + # #1042 bypass shape (--admin) chained after the comment body -> BLOCK. |
| 280 | + from merge_guard_pre import ( |
| 281 | + is_compound_destructive_command, |
| 282 | + _strip_non_executable_content, |
| 283 | + ) |
| 284 | + |
| 285 | + cmd = 'gh pr comment 5 --body "ok" ; gh pr merge 5 --admin' |
| 286 | + assert is_compound_destructive_command(cmd) |
| 287 | + assert "gh pr merge 5 --admin" in _strip_non_executable_content(cmd) |
| 288 | + |
| 289 | + def test_output_procsub_blocks_defense_in_depth(self): |
| 290 | + # `>(bash -c ...)` output process-sub: BLOCK via DEFENSE-IN-DEPTH — the |
| 291 | + # whole-command process-sub-to-shell guard skips the carrier-strip entirely |
| 292 | + # AND the op is outside any flag-anchored value. Stays BLOCK under the |
| 293 | + # whole-segment-loosen alone (a combined procsub-guard-neuter is needed too). |
| 294 | + from merge_guard_pre import is_dangerous_command, _strip_non_executable_content |
| 295 | + |
| 296 | + cmd = 'gh pr comment 5 --body "x" >(bash -c "gh pr merge 9")' |
| 297 | + assert is_dangerous_command(cmd) |
| 298 | + assert "gh pr merge 9" in _strip_non_executable_content(cmd) |
0 commit comments