@ictechgy/context-guard 0.4.16 → 0.6.0
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/CHANGELOG.md +74 -0
- package/README.ko.md +91 -1
- package/README.md +95 -1
- package/docs/distribution.md +100 -0
- package/package.json +5 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +34 -1
- package/plugins/context-guard/README.md +36 -1
- package/plugins/context-guard/bin/bash_reference_policy.py +967 -0
- package/plugins/context-guard/bin/context-guard-artifact +1 -1
- package/plugins/context-guard/bin/context-guard-bench +4216 -103
- package/plugins/context-guard/bin/context-guard-failed-nudge +95 -23
- package/plugins/context-guard/bin/context-guard-guard-read +6 -2
- package/plugins/context-guard/bin/context-guard-mcp +2 -1
- package/plugins/context-guard/bin/context-guard-pack +2086 -142
- package/plugins/context-guard/bin/context-guard-rewrite-bash +497 -45
- package/plugins/context-guard/bin/context-guard-sanitize-output +178 -22
- package/plugins/context-guard/bin/context-guard-setup +901 -28
- package/plugins/context-guard/bin/context-guard-statusline +33 -2
- package/plugins/context-guard/bin/context-guard-statusline-merged +71 -20
- package/plugins/context-guard/bin/context-guard-task-memory +635 -0
- package/plugins/context-guard/bin/context-guard-trim-output +706 -35
- package/plugins/context-guard/lib/context_guard_commands.py +25 -1
- package/plugins/context-guard/lib/context_pack_git_boundary.py +19 -0
- package/plugins/context-guard/lib/context_pack_identity.py +115 -0
- package/plugins/context-guard/lib/context_pack_receipts.py +9 -0
- package/plugins/context-guard/lib/context_pack_rendering.py +12 -0
- package/plugins/context-guard/lib/context_pack_scanning.py +10 -0
- package/plugins/context-guard/lib/context_pack_selection.py +6 -0
- package/plugins/context-guard/lib/credential_policy.py +10 -2
|
@@ -17,6 +17,7 @@ import os
|
|
|
17
17
|
from pathlib import Path, PurePosixPath
|
|
18
18
|
import queue
|
|
19
19
|
import re
|
|
20
|
+
import shutil
|
|
20
21
|
import signal
|
|
21
22
|
import subprocess
|
|
22
23
|
import sys
|
|
@@ -26,6 +27,20 @@ from types import ModuleType
|
|
|
26
27
|
from typing import BinaryIO, Iterable, Iterator, NamedTuple, TextIO
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
WRAPPER_ENV_DENY = frozenset({
|
|
31
|
+
"BASHOPTS",
|
|
32
|
+
"BASH_ENV",
|
|
33
|
+
"BASH_XTRACEFD",
|
|
34
|
+
"ENV",
|
|
35
|
+
"PS4",
|
|
36
|
+
"PYTHONHOME",
|
|
37
|
+
"PYTHONPATH",
|
|
38
|
+
"PYTHONSTARTUP",
|
|
39
|
+
"SHELLOPTS",
|
|
40
|
+
})
|
|
41
|
+
BASH_FUNCTION_ENV_RE = re.compile(r"^BASH_FUNC_.*%%$")
|
|
42
|
+
|
|
43
|
+
|
|
29
44
|
def load_credential_policy() -> ModuleType:
|
|
30
45
|
script_dir = Path(__file__).resolve().parent
|
|
31
46
|
candidate = (
|
|
@@ -110,10 +125,11 @@ COOKIE_HEADER_RE = re.compile(
|
|
|
110
125
|
# 복원한다. 이 형태가 예전 형태와 언어·스팬·그룹 모두에서 동치임은
|
|
111
126
|
# tests/test_sanitize_output_redos.py의 차분 배터리로 고정한다.
|
|
112
127
|
QUOTED_VALUE_BODY = r"(?:\\.|(?!(?P=quote))[^\\])*\\?"
|
|
128
|
+
SECRET_ASSIGNMENT_SEPARATOR = r"[ \t]*[:=][ \t]*"
|
|
113
129
|
INLINE_QUOTED_SECRET_ASSIGNMENT_RE = re.compile(
|
|
114
130
|
rf"(?i)(?P<lead>^|[\s;{{\[,])"
|
|
115
131
|
rf"(?P<prefix>(?:(?:[^:\n]+):\d+(?::\d+)?:)?\s*(?:[+-]\s*)?(?:export\s+)?"
|
|
116
|
-
rf"[\"']?(?:{SECRET_KEY})[\"']
|
|
132
|
+
rf"[\"']?(?:{SECRET_KEY})[\"']?{SECRET_ASSIGNMENT_SEPARATOR})"
|
|
117
133
|
rf"(?P<quote>[\"'])(?P<value>{QUOTED_VALUE_BODY})(?P=quote)(?P<tail>[^\s,;}}\]]*)"
|
|
118
134
|
)
|
|
119
135
|
CODE_IDENTIFIER = r"[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*"
|
|
@@ -121,7 +137,7 @@ CALL_ARGUMENT_CHUNK = r"(?:[^()\"'\n;]|\"(?:\\.|[^\"\\])*\"|'(?:\\.|[^'\\])*'|\(
|
|
|
121
137
|
INLINE_UNQUOTED_CALL_SECRET_ASSIGNMENT_RE = re.compile(
|
|
122
138
|
rf"(?i)(?P<lead>^|[\s;{{\[,])"
|
|
123
139
|
rf"(?P<prefix>(?:(?:[^:\n]+):\d+(?::\d+)?:)?\s*(?:[+-]\s*)?(?:export\s+)?"
|
|
124
|
-
rf"[\"']?(?:{SECRET_KEY})[\"']
|
|
140
|
+
rf"[\"']?(?:{SECRET_KEY})[\"']?{SECRET_ASSIGNMENT_SEPARATOR})"
|
|
125
141
|
rf"(?P<value>(?![\"']){CODE_IDENTIFIER}\({CALL_ARGUMENT_CHUNK}\))"
|
|
126
142
|
)
|
|
127
143
|
SECRET_IDENTIFIER_PART = (
|
|
@@ -133,7 +149,7 @@ FALLBACK_SECRET_OPERAND = rf"(?:[A-Za-z_$][A-Za-z0-9_$]*\.)*{SECRET_IDENTIFIER_P
|
|
|
133
149
|
INLINE_UNQUOTED_FALLBACK_SECRET_ASSIGNMENT_RE = re.compile(
|
|
134
150
|
rf"(?i)(?P<lead>^|[\s;{{\[,])"
|
|
135
151
|
rf"(?P<prefix>(?:(?:[^:\n]+):\d+(?::\d+)?:)?\s*(?:[+-]\s*)?(?:export\s+)?"
|
|
136
|
-
rf"[\"']?(?:{SECRET_KEY})[\"']
|
|
152
|
+
rf"[\"']?(?:{SECRET_KEY})[\"']?{SECRET_ASSIGNMENT_SEPARATOR})"
|
|
137
153
|
rf"(?P<value>(?![\"']|\[REDACTED\])"
|
|
138
154
|
rf"[^;\n]*?(?:\bor\b|\|\||\?\?|\belse\b|\?[^:\n;]*:)\s*"
|
|
139
155
|
rf"(?:[\"'](?:\\.|[^\"'\\])*[\"']|{FALLBACK_SECRET_OPERAND})[^;\n]*)"
|
|
@@ -141,20 +157,32 @@ INLINE_UNQUOTED_FALLBACK_SECRET_ASSIGNMENT_RE = re.compile(
|
|
|
141
157
|
INLINE_UNQUOTED_BRACKETED_SECRET_ASSIGNMENT_RE = re.compile(
|
|
142
158
|
rf"(?i)(?P<lead>^|[\s;{{\[,])"
|
|
143
159
|
rf"(?P<prefix>(?:(?:[^:\n]+):\d+(?::\d+)?:)?\s*(?:[+-]\s*)?(?:export\s+)?"
|
|
144
|
-
rf"[\"']?(?:{SECRET_KEY})[\"']
|
|
160
|
+
rf"[\"']?(?:{SECRET_KEY})[\"']?{SECRET_ASSIGNMENT_SEPARATOR})"
|
|
145
161
|
rf"(?P<value>(?![\"']|\[REDACTED\])"
|
|
146
162
|
rf"[^\s,;}}\]]*(?:\([^;\n]*?\)|\{{[^;\n]*?\}}|\[[^;\n]*?\])[^\s,;}}\]]*)"
|
|
147
163
|
)
|
|
148
164
|
INLINE_UNQUOTED_SECRET_ASSIGNMENT_RE = re.compile(
|
|
149
165
|
rf"(?i)(?P<lead>^|[\s;{{\[,])"
|
|
150
166
|
rf"(?P<prefix>(?:(?:[^:\n]+):\d+(?::\d+)?:)?\s*(?:[+-]\s*)?(?:export\s+)?"
|
|
151
|
-
rf"[\"']?(?:{SECRET_KEY})[\"']
|
|
167
|
+
rf"[\"']?(?:{SECRET_KEY})[\"']?{SECRET_ASSIGNMENT_SEPARATOR})"
|
|
152
168
|
rf"(?P<value>(?![\"']|\[REDACTED\])[^\s,;}}\]]+)"
|
|
153
169
|
)
|
|
170
|
+
WHITESPACE_SECRET_ASSIGNMENT_PREFIX = (
|
|
171
|
+
rf"(?P<lead>\A)(?P<prefix>[ \t]*(?:[+-][ \t]*)?(?:export[ \t]+)?"
|
|
172
|
+
rf"[\"']?(?:{SECRET_KEY})[\"']?(?![ \t]*[:=])[ \t]+)"
|
|
173
|
+
)
|
|
174
|
+
WHITESPACE_QUOTED_SECRET_ASSIGNMENT_NO_LOCATION_RE = re.compile(
|
|
175
|
+
rf"(?i){WHITESPACE_SECRET_ASSIGNMENT_PREFIX}"
|
|
176
|
+
rf"(?P<quote>[\"'])(?P<value>{QUOTED_VALUE_BODY})(?P=quote)(?P<tail>[^\s,;}}\]]*)"
|
|
177
|
+
)
|
|
178
|
+
WHITESPACE_UNQUOTED_SECRET_ASSIGNMENT_NO_LOCATION_RE = re.compile(
|
|
179
|
+
rf"(?i){WHITESPACE_SECRET_ASSIGNMENT_PREFIX}"
|
|
180
|
+
rf"(?P<value>(?![\"']|\[REDACTED\])\S[^\n]*)"
|
|
181
|
+
)
|
|
154
182
|
UNQUOTED_MULTILINE_SECRET_ASSIGNMENT_RE = re.compile(
|
|
155
183
|
rf"(?i)(?:^|[\s;{{\[,])"
|
|
156
184
|
rf"(?:(?:[^:\n]+):\d+(?::\d+)?:)?\s*(?:[+-]\s*)?(?:export\s+)?"
|
|
157
|
-
rf"[\"']?(?:{SECRET_KEY})[\"']
|
|
185
|
+
rf"[\"']?(?:{SECRET_KEY})[\"']?{SECRET_ASSIGNMENT_SEPARATOR}(?P<value>(?![\"']).*)$"
|
|
158
186
|
)
|
|
159
187
|
# F-14 one-pass scanner.
|
|
160
188
|
#
|
|
@@ -181,7 +209,11 @@ LOCATION_PREFIX_SCAN_RE = re.compile(
|
|
|
181
209
|
# 경로로 인정하되, 그 스팬도 함께 검사한다. 예: "src/it's.py:5:api_key='x'".
|
|
182
210
|
LOCATION_PREFIX_ASSIGNMENT_SIGNAL_RE = re.compile(
|
|
183
211
|
rf"(?i)(?:-----BEGIN|(?:Proxy-)?Authorization\s*:|(?:Set-)?Cookie\s*:"
|
|
184
|
-
rf"|[\"']?(?:{SECRET_KEY})[\"']
|
|
212
|
+
rf"|[\"']?(?:{SECRET_KEY})[\"']?{SECRET_ASSIGNMENT_SEPARATOR})"
|
|
213
|
+
)
|
|
214
|
+
LOCATION_PREFIX_WHITESPACE_ASSIGNMENT_SIGNAL_RE = re.compile(
|
|
215
|
+
rf"(?i)\A[ \t]*(?:[+-][ \t]*)?(?:export[ \t]+)?"
|
|
216
|
+
rf"[\"']?(?:{SECRET_KEY})[\"']?(?![ \t]*[:=])[ \t]+"
|
|
185
217
|
)
|
|
186
218
|
LOCATION_PREFIX_PATH_ONLY_SIGNAL_RE = re.compile(r"[\"']")
|
|
187
219
|
NINE_LOCATION_CONSUMERS = (
|
|
@@ -246,7 +278,10 @@ def scan_location_prefix(line: str) -> ScannedLine:
|
|
|
246
278
|
return ScannedLine("", line, 0)
|
|
247
279
|
end = match.end()
|
|
248
280
|
candidate = line[:end]
|
|
249
|
-
if
|
|
281
|
+
if (
|
|
282
|
+
LOCATION_PREFIX_ASSIGNMENT_SIGNAL_RE.search(candidate)
|
|
283
|
+
or LOCATION_PREFIX_WHITESPACE_ASSIGNMENT_SIGNAL_RE.match(candidate)
|
|
284
|
+
):
|
|
250
285
|
# 값의 일부를 경로로 오인한 경우다. 분리하면 앞부분만 가려져 뒤가 남는다.
|
|
251
286
|
return ScannedLine("", line, 0, False)
|
|
252
287
|
declined = bool(LOCATION_PREFIX_PATH_ONLY_SIGNAL_RE.search(candidate))
|
|
@@ -312,8 +347,33 @@ SAFE_GETTER_KEY_NAMES = {
|
|
|
312
347
|
"sid",
|
|
313
348
|
"token",
|
|
314
349
|
}
|
|
350
|
+
AMBIGUOUS_BARE_WHITESPACE_KEYS = frozenset(
|
|
351
|
+
{
|
|
352
|
+
"auth",
|
|
353
|
+
"authorization",
|
|
354
|
+
"cookie",
|
|
355
|
+
"credential",
|
|
356
|
+
"credentials",
|
|
357
|
+
"csrf",
|
|
358
|
+
"jwt",
|
|
359
|
+
"pass",
|
|
360
|
+
"password",
|
|
361
|
+
"passwd",
|
|
362
|
+
"pwd",
|
|
363
|
+
"secret",
|
|
364
|
+
"session",
|
|
365
|
+
"sid",
|
|
366
|
+
"sig",
|
|
367
|
+
"signature",
|
|
368
|
+
"token",
|
|
369
|
+
"xsrf",
|
|
370
|
+
}
|
|
371
|
+
)
|
|
315
372
|
ASSIGNMENT_KEY_RE = re.compile(
|
|
316
|
-
|
|
373
|
+
rf"(?P<key>[A-Za-z_$][A-Za-z0-9_$.-]*)[\"']?{SECRET_ASSIGNMENT_SEPARATOR}$"
|
|
374
|
+
)
|
|
375
|
+
WHITESPACE_ASSIGNMENT_KEY_RE = re.compile(
|
|
376
|
+
r"(?P<key>[A-Za-z_$][A-Za-z0-9_$.-]*)[\"']?[ \t]+$"
|
|
317
377
|
)
|
|
318
378
|
SOURCE_SAFE_VALUE_RE = re.compile(
|
|
319
379
|
r"^(?:"
|
|
@@ -534,14 +594,32 @@ def normalize_getter_key(key: str) -> str:
|
|
|
534
594
|
|
|
535
595
|
|
|
536
596
|
def assignment_key(prefix: str) -> str | None:
|
|
537
|
-
|
|
538
|
-
|
|
597
|
+
for pattern in (ASSIGNMENT_KEY_RE, WHITESPACE_ASSIGNMENT_KEY_RE):
|
|
598
|
+
match = pattern.search(prefix)
|
|
599
|
+
if match is not None:
|
|
600
|
+
return match.group("key")
|
|
601
|
+
return None
|
|
539
602
|
|
|
540
603
|
|
|
541
604
|
def is_safe_getter_key(key: str) -> bool:
|
|
542
605
|
return normalize_getter_key(key) in SAFE_GETTER_KEY_NAMES
|
|
543
606
|
|
|
544
607
|
|
|
608
|
+
def is_ambiguous_bare_whitespace_key(key: str) -> bool:
|
|
609
|
+
return normalize_sensitive_key(key.strip().strip("\"'")) in AMBIGUOUS_BARE_WHITESPACE_KEYS
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def should_redact_whitespace_secret_value(key: str, value: str) -> bool:
|
|
613
|
+
stripped_key = key.strip().strip("\"'")
|
|
614
|
+
normalized_key = normalize_sensitive_key(stripped_key)
|
|
615
|
+
if normalized_key == "pass" and stripped_key != "pass":
|
|
616
|
+
return False
|
|
617
|
+
return not (
|
|
618
|
+
normalized_key in AMBIGUOUS_BARE_WHITESPACE_KEYS
|
|
619
|
+
and re.search(r"[ \t]", value.strip()) is not None
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
|
|
545
623
|
def should_redact_unquoted_secret_value(
|
|
546
624
|
line: str,
|
|
547
625
|
match: re.Match[str],
|
|
@@ -601,11 +679,20 @@ def redact_secret_assignments(
|
|
|
601
679
|
redacted = True
|
|
602
680
|
return f"{match.group('lead')}{match.group('prefix')}{match.group('quote')}[REDACTED]{match.group('quote')}"
|
|
603
681
|
|
|
604
|
-
def unquoted_repl(
|
|
682
|
+
def unquoted_repl(
|
|
683
|
+
match: re.Match[str],
|
|
684
|
+
*,
|
|
685
|
+
whitespace_separator: bool = False,
|
|
686
|
+
) -> str:
|
|
605
687
|
nonlocal redacted
|
|
606
688
|
key = assignment_key(match.group("prefix"))
|
|
607
689
|
if key is None or not is_sensitive_key(key):
|
|
608
690
|
return match.group(0)
|
|
691
|
+
if whitespace_separator and not should_redact_whitespace_secret_value(
|
|
692
|
+
key,
|
|
693
|
+
match.group("value"),
|
|
694
|
+
):
|
|
695
|
+
return match.group(0)
|
|
609
696
|
if not should_redact_unquoted_secret_value(line, match, context=context):
|
|
610
697
|
return match.group(0)
|
|
611
698
|
redacted = True
|
|
@@ -618,6 +705,11 @@ def redact_secret_assignments(
|
|
|
618
705
|
span = INLINE_UNQUOTED_FALLBACK_SECRET_ASSIGNMENT_NO_LOCATION_RE.sub(unquoted_repl, span)
|
|
619
706
|
span = INLINE_UNQUOTED_CALL_SECRET_ASSIGNMENT_NO_LOCATION_RE.sub(unquoted_repl, span)
|
|
620
707
|
span = INLINE_UNQUOTED_BRACKETED_SECRET_ASSIGNMENT_NO_LOCATION_RE.sub(unquoted_repl, span)
|
|
708
|
+
span = WHITESPACE_QUOTED_SECRET_ASSIGNMENT_NO_LOCATION_RE.sub(quoted_repl, span)
|
|
709
|
+
span = WHITESPACE_UNQUOTED_SECRET_ASSIGNMENT_NO_LOCATION_RE.sub(
|
|
710
|
+
lambda match: unquoted_repl(match, whitespace_separator=True),
|
|
711
|
+
span,
|
|
712
|
+
)
|
|
621
713
|
span = INLINE_UNQUOTED_SECRET_ASSIGNMENT_NO_LOCATION_RE.sub(unquoted_repl, span)
|
|
622
714
|
return span
|
|
623
715
|
|
|
@@ -630,7 +722,15 @@ def redact_secret_assignments(
|
|
|
630
722
|
|
|
631
723
|
MULTILINE_SECRET_ASSIGNMENT_RE = re.compile(
|
|
632
724
|
rf"(?i)(?:^|[\s;{{\[,])(?:(?:[^:\n]+):\d+(?::\d+)?:)?\s*(?:[+-]\s*)?(?:export\s+)?"
|
|
633
|
-
rf"[\"']?(?P<key>{SECRET_KEY})[\"']
|
|
725
|
+
rf"[\"']?(?P<key>{SECRET_KEY})[\"']?{SECRET_ASSIGNMENT_SEPARATOR}(?P<quote>[\"'])"
|
|
726
|
+
)
|
|
727
|
+
WHITESPACE_MULTILINE_SECRET_ASSIGNMENT_NO_LOCATION_RE = re.compile(
|
|
728
|
+
rf"(?i)\A[ \t]*(?:[+-][ \t]*)?(?:export[ \t]+)?"
|
|
729
|
+
rf"[\"']?(?P<key>{SECRET_KEY})[\"']?(?![ \t]*[:=])[ \t]+(?P<quote>[\"'])"
|
|
730
|
+
)
|
|
731
|
+
WHITESPACE_UNQUOTED_MULTILINE_SECRET_ASSIGNMENT_NO_LOCATION_RE = re.compile(
|
|
732
|
+
rf"(?i)\A[ \t]*(?:[+-][ \t]*)?(?:export[ \t]+)?"
|
|
733
|
+
rf"[\"']?(?:{SECRET_KEY})[\"']?(?![ \t]*[:=])[ \t]+(?P<value>(?![\"']).*)$"
|
|
634
734
|
)
|
|
635
735
|
|
|
636
736
|
|
|
@@ -702,12 +802,19 @@ def detect_multiline_secret_assignment(line: str) -> str | None:
|
|
|
702
802
|
# 보아 location prefix 바로 뒤에 붙은 키(콜론 직후)까지 잡는다. 둘 다 한 번씩이라 선형.
|
|
703
803
|
spans = (line, scan.remainder) if scan.prefix else (line,)
|
|
704
804
|
for span in spans:
|
|
705
|
-
for
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
805
|
+
for pattern, whitespace_separator in (
|
|
806
|
+
(MULTILINE_SECRET_ASSIGNMENT_NO_LOCATION_RE, False),
|
|
807
|
+
(WHITESPACE_MULTILINE_SECRET_ASSIGNMENT_NO_LOCATION_RE, True),
|
|
808
|
+
):
|
|
809
|
+
for marker in pattern.finditer(span):
|
|
810
|
+
key = marker.group("key")
|
|
811
|
+
if not is_sensitive_key(key):
|
|
812
|
+
continue
|
|
813
|
+
if whitespace_separator and is_ambiguous_bare_whitespace_key(key):
|
|
814
|
+
continue
|
|
815
|
+
quote = marker.group("quote")
|
|
816
|
+
if not has_unescaped_quote(span, quote, marker.end("quote")):
|
|
817
|
+
return quote
|
|
711
818
|
return None
|
|
712
819
|
|
|
713
820
|
|
|
@@ -744,9 +851,15 @@ def detect_multiline_secret_expression(line: str) -> int | None:
|
|
|
744
851
|
spans = (line, scan.remainder) if scan.prefix else (line,)
|
|
745
852
|
marker = None
|
|
746
853
|
for span in spans:
|
|
747
|
-
|
|
854
|
+
for pattern, whitespace_separator in (
|
|
855
|
+
(UNQUOTED_MULTILINE_SECRET_ASSIGNMENT_NO_LOCATION_RE, False),
|
|
856
|
+
(WHITESPACE_UNQUOTED_MULTILINE_SECRET_ASSIGNMENT_NO_LOCATION_RE, True),
|
|
857
|
+
):
|
|
858
|
+
marker = pattern.search(span)
|
|
859
|
+
if marker is not None:
|
|
860
|
+
line = span
|
|
861
|
+
break
|
|
748
862
|
if marker is not None:
|
|
749
|
-
line = span
|
|
750
863
|
break
|
|
751
864
|
if marker is None:
|
|
752
865
|
return None
|
|
@@ -754,6 +867,8 @@ def detect_multiline_secret_expression(line: str) -> int | None:
|
|
|
754
867
|
key = assignment_key(prefix)
|
|
755
868
|
if key is None or not is_sensitive_key(key):
|
|
756
869
|
return None
|
|
870
|
+
if whitespace_separator and is_ambiguous_bare_whitespace_key(key):
|
|
871
|
+
return None
|
|
757
872
|
value = marker.group("value").strip()
|
|
758
873
|
if not value:
|
|
759
874
|
return 0
|
|
@@ -1233,6 +1348,7 @@ def run_command(
|
|
|
1233
1348
|
timeout_seconds: int,
|
|
1234
1349
|
*,
|
|
1235
1350
|
max_line_chars: int = MAX_LINE_CHARS_LIMIT,
|
|
1351
|
+
environment: dict[str, str] | None = None,
|
|
1236
1352
|
) -> tuple[Iterable[str], subprocess.Popen[bytes] | None, int | None]:
|
|
1237
1353
|
popen_kwargs: dict[str, object] = {}
|
|
1238
1354
|
if os.name != "nt":
|
|
@@ -1240,6 +1356,7 @@ def run_command(
|
|
|
1240
1356
|
try:
|
|
1241
1357
|
proc = subprocess.Popen(
|
|
1242
1358
|
command,
|
|
1359
|
+
env=environment,
|
|
1243
1360
|
stdout=subprocess.PIPE,
|
|
1244
1361
|
stderr=subprocess.STDOUT,
|
|
1245
1362
|
text=False,
|
|
@@ -1265,6 +1382,14 @@ def run_command(
|
|
|
1265
1382
|
)
|
|
1266
1383
|
|
|
1267
1384
|
|
|
1385
|
+
def sanitized_wrapper_environment() -> dict[str, str]:
|
|
1386
|
+
environment = os.environ.copy()
|
|
1387
|
+
for name in tuple(environment):
|
|
1388
|
+
if name in WRAPPER_ENV_DENY or BASH_FUNCTION_ENV_RE.fullmatch(name):
|
|
1389
|
+
environment.pop(name, None)
|
|
1390
|
+
return environment
|
|
1391
|
+
|
|
1392
|
+
|
|
1268
1393
|
def stdin_has_data(stdin: TextIO) -> bool:
|
|
1269
1394
|
return not stdin.isatty()
|
|
1270
1395
|
|
|
@@ -1355,13 +1480,43 @@ def main() -> int:
|
|
|
1355
1480
|
command = args.command
|
|
1356
1481
|
if command and command[0] == "--":
|
|
1357
1482
|
command = command[1:]
|
|
1483
|
+
command_environment: dict[str, str] | None = None
|
|
1358
1484
|
if args.wrapper_context is not None:
|
|
1359
|
-
|
|
1485
|
+
env_runtime = shutil.which("env", path=os.defpath)
|
|
1486
|
+
bash_runtime = shutil.which("bash", path=os.defpath)
|
|
1487
|
+
trusted_prefix = [
|
|
1488
|
+
os.path.realpath(env_runtime) if env_runtime else "",
|
|
1489
|
+
"-u", "BASH_ENV",
|
|
1490
|
+
"-u", "ENV",
|
|
1491
|
+
"-u", "PYTHONHOME",
|
|
1492
|
+
"-u", "PYTHONPATH",
|
|
1493
|
+
"-u", "PYTHONSTARTUP",
|
|
1494
|
+
"-u", "SHELLOPTS",
|
|
1495
|
+
"-u", "BASHOPTS",
|
|
1496
|
+
"-u", "PS4",
|
|
1497
|
+
os.path.realpath(bash_runtime) if bash_runtime else "",
|
|
1498
|
+
"--noprofile",
|
|
1499
|
+
"--norc",
|
|
1500
|
+
"-p",
|
|
1501
|
+
"-c",
|
|
1502
|
+
]
|
|
1503
|
+
legacy_shape = len(command) == 3 and command[0:2] == ["bash", "-c"] and bool(command[2])
|
|
1504
|
+
trusted_shape = (
|
|
1505
|
+
bool(env_runtime)
|
|
1506
|
+
and bool(bash_runtime)
|
|
1507
|
+
and len(command) == len(trusted_prefix) + 1
|
|
1508
|
+
and command[:-1] == trusted_prefix
|
|
1509
|
+
and bool(command[-1])
|
|
1510
|
+
)
|
|
1511
|
+
if not legacy_shape and not trusted_shape:
|
|
1360
1512
|
print(
|
|
1361
1513
|
"context-guard-sanitize-output: invalid context-guard wrapper v1 shape",
|
|
1362
1514
|
file=sys.stderr,
|
|
1363
1515
|
)
|
|
1364
1516
|
return 2
|
|
1517
|
+
if legacy_shape:
|
|
1518
|
+
command = [*trusted_prefix, command[2]]
|
|
1519
|
+
command_environment = sanitized_wrapper_environment()
|
|
1365
1520
|
context = coerce_sanitization_context(
|
|
1366
1521
|
args.wrapper_context,
|
|
1367
1522
|
private_roots=args.private_root,
|
|
@@ -1388,6 +1543,7 @@ def main() -> int:
|
|
|
1388
1543
|
command,
|
|
1389
1544
|
args.timeout_seconds,
|
|
1390
1545
|
max_line_chars=COMMAND_MAX_UNTERMINATED_LINE_CHARS,
|
|
1546
|
+
environment=command_environment,
|
|
1391
1547
|
)
|
|
1392
1548
|
if isinstance(stream, TimedCommandStream):
|
|
1393
1549
|
command_stream = stream
|