plugin-scanner 2.2.112__py3-none-any.whl → 2.2.113__py3-none-any.whl
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.
- codex_plugin_scanner/guard/runtime/command_builtin_extension_registry.py +7 -3
- codex_plugin_scanner/guard/runtime/command_builtin_rules.py +8 -0
- codex_plugin_scanner/guard/runtime/compound_git_inspection.py +1 -1
- codex_plugin_scanner/guard/runtime/git_index_inspection.py +408 -0
- codex_plugin_scanner/guard/runtime/secret_file_request_services/developer_routines.py +3 -2
- codex_plugin_scanner/guard/runtime/secret_file_request_services/source_edit_context.py +10 -5
- codex_plugin_scanner/guard/runtime/secret_file_request_services/tool_action_requests.py +20 -2
- codex_plugin_scanner/version.py +1 -1
- {plugin_scanner-2.2.112.dist-info → plugin_scanner-2.2.113.dist-info}/METADATA +1 -1
- {plugin_scanner-2.2.112.dist-info → plugin_scanner-2.2.113.dist-info}/RECORD +13 -12
- {plugin_scanner-2.2.112.dist-info → plugin_scanner-2.2.113.dist-info}/WHEEL +0 -0
- {plugin_scanner-2.2.112.dist-info → plugin_scanner-2.2.113.dist-info}/entry_points.txt +0 -0
- {plugin_scanner-2.2.112.dist-info → plugin_scanner-2.2.113.dist-info}/licenses/LICENSE +0 -0
|
@@ -53,12 +53,16 @@ _CORE_COMMAND_EXTENSION_SPECS: Final[tuple[CommandExtensionSpec, ...]] = (
|
|
|
53
53
|
CommandExtensionSpec(
|
|
54
54
|
extension_id="command.git",
|
|
55
55
|
name="Git protection",
|
|
56
|
-
description=
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
description=(
|
|
57
|
+
"Reviews local and remote Git operations that can discard work, replace history, "
|
|
58
|
+
"or read a staged index Guard cannot bound."
|
|
59
|
+
),
|
|
60
|
+
action_classes=("git destructive command", "git index inspection"),
|
|
61
|
+
risk_classes=("destructive_shell", "local_secret_read"),
|
|
59
62
|
safer_alternatives=(
|
|
60
63
|
"Preview affected files and refs before destructive repository operations.",
|
|
61
64
|
"Create a temporary branch or stash before discarding local work.",
|
|
65
|
+
"Inspect the staged index with git diff --cached --check or lockfile excludes.",
|
|
62
66
|
),
|
|
63
67
|
reference_urls=(),
|
|
64
68
|
required=True,
|
|
@@ -42,6 +42,7 @@ COMMAND_ACTION_RISK_CLASSES: dict[str, tuple[str, ...]] = {
|
|
|
42
42
|
"github pr body shell substitution": ("execution",),
|
|
43
43
|
"filesystem destructive command": ("destructive_shell",),
|
|
44
44
|
"git destructive command": ("destructive_shell",),
|
|
45
|
+
"git index inspection": ("local_secret_read",),
|
|
45
46
|
"system destructive command": ("destructive_shell",),
|
|
46
47
|
"windows destructive command": ("destructive_shell",),
|
|
47
48
|
"kubernetes destructive command": ("destructive_shell", "network_egress"),
|
|
@@ -243,6 +244,13 @@ BUILT_IN_COMMAND_RULES = (
|
|
|
243
244
|
action_class="GitHub PR body shell substitution",
|
|
244
245
|
safer_alternative="Use a literal body file whose contents can be reviewed before submission.",
|
|
245
246
|
),
|
|
247
|
+
_compatibility_rule(
|
|
248
|
+
rule_id="command.git.index-inspection",
|
|
249
|
+
title="Git index inspection",
|
|
250
|
+
description="Identifies staged-index Git reads that Guard cannot verify as a bounded inspection.",
|
|
251
|
+
action_class="git index inspection",
|
|
252
|
+
safer_alternative="Run git diff --cached --check, or exclude lockfiles and scan the staged patch on stdin.",
|
|
253
|
+
),
|
|
246
254
|
_structured_rule(
|
|
247
255
|
rule_id="command.filesystem.recursive-delete",
|
|
248
256
|
title="Recursive filesystem deletion",
|
|
@@ -131,7 +131,7 @@ def is_low_risk_git_inspection_segment(segment: ShellExecutionSegment) -> bool:
|
|
|
131
131
|
if operation == "rev-parse":
|
|
132
132
|
return _safe_rev_parse_args(args)
|
|
133
133
|
if operation == "diff":
|
|
134
|
-
return _safe_diff_args(args)
|
|
134
|
+
return _safe_diff_args(args) and _git_show_has_execution_free_config(segment, repository_path=repository_path)
|
|
135
135
|
if operation == "ls-files":
|
|
136
136
|
return _safe_ls_files_args(args)
|
|
137
137
|
if operation == "show":
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""Staged-index Git inspection recognition for allow and extension ownership."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Final, Literal
|
|
9
|
+
|
|
10
|
+
from .compound_git_inspection import (
|
|
11
|
+
_git_show_has_execution_free_config,
|
|
12
|
+
_safe_repository_path,
|
|
13
|
+
is_low_risk_git_inspection_segment,
|
|
14
|
+
is_low_risk_standalone_git_routine,
|
|
15
|
+
)
|
|
16
|
+
from .secret_file_request_services.shell_tokenization import _shell_segment_primary_command
|
|
17
|
+
from .shell_execution_context import ShellExecutionContext, ShellExecutionSegment, model_shell_execution_context
|
|
18
|
+
|
|
19
|
+
_CONTROL_PREFIXES: Final = frozenset({"!", "elif", "else", "fi", "if", "then"})
|
|
20
|
+
_ALLOWED_CONTROLS: Final = frozenset({"&&", "||", "|", ";", "\n"})
|
|
21
|
+
_RG_BOOLEAN_FLAGS: Final = frozenset({"--ignore-case", "--line-number", "--no-config", "-i", "-in", "-n", "-ni"})
|
|
22
|
+
_GIT_GLOBAL_FLAG_OPTIONS: Final = frozenset(
|
|
23
|
+
{
|
|
24
|
+
"--literal-pathspecs",
|
|
25
|
+
"--no-advice",
|
|
26
|
+
"--no-lazy-fetch",
|
|
27
|
+
"--no-optional-locks",
|
|
28
|
+
"--no-pager",
|
|
29
|
+
"--no-replace-objects",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
_GIT_GLOBAL_VALUE_OPTIONS: Final = frozenset(
|
|
33
|
+
{"--config-env", "--exec-path", "--git-dir", "--namespace", "--super-prefix", "--work-tree", "-C", "-c"}
|
|
34
|
+
)
|
|
35
|
+
_EXECUTION_ROUTING_OPTIONS: Final = frozenset({"-c", "--config-env", "--exec-path"})
|
|
36
|
+
_REPOSITORY_SELECTOR_OPTIONS: Final = frozenset(
|
|
37
|
+
{"--bare", "--git-dir", "--namespace", "--paginate", "--super-prefix", "--work-tree", "-P", "-p"}
|
|
38
|
+
)
|
|
39
|
+
_INDEX_DIFF_FLAGS: Final = frozenset({"--cached", "--staged"})
|
|
40
|
+
_CACHED_DIFF_KIND = Literal["owned", "routed"]
|
|
41
|
+
_MAX_SEGMENTS: Final = 32
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def executable_tokens(tokens: tuple[str, ...]) -> tuple[str, ...] | None:
|
|
45
|
+
"""Strip shell reserved-word prefixes. Empty means a no-op such as fi."""
|
|
46
|
+
|
|
47
|
+
index = 0
|
|
48
|
+
while index < len(tokens) and tokens[index] in _CONTROL_PREFIXES:
|
|
49
|
+
if tokens[index] == "fi" and len(tokens) != 1:
|
|
50
|
+
return None
|
|
51
|
+
index += 1
|
|
52
|
+
return tokens[index:]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _static_echo_arg_is_safe(arg: str) -> bool:
|
|
56
|
+
if "`" in arg or "$(" in arg or "<(" in arg or ">(" in arg:
|
|
57
|
+
return False
|
|
58
|
+
return "$" not in arg.replace("$?", "")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _index_scan_rg_args_are_safe(args: tuple[str, ...]) -> bool:
|
|
62
|
+
"""Accept stdin-only ripgrep with one inline pattern after a cached diff."""
|
|
63
|
+
|
|
64
|
+
if os.environ.get("RIPGREP_CONFIG_PATH") and "--no-config" not in args:
|
|
65
|
+
return False
|
|
66
|
+
saw_pattern = False
|
|
67
|
+
for arg in args:
|
|
68
|
+
if arg.startswith("RIPGREP_CONFIG_PATH="):
|
|
69
|
+
return False
|
|
70
|
+
if arg in _RG_BOOLEAN_FLAGS:
|
|
71
|
+
continue
|
|
72
|
+
if not arg or arg.startswith("-"):
|
|
73
|
+
return False
|
|
74
|
+
if saw_pattern:
|
|
75
|
+
return False
|
|
76
|
+
if "$" in arg or any(marker in arg for marker in ("$(", "`", "<(", ">(", "\x00")):
|
|
77
|
+
return False
|
|
78
|
+
saw_pattern = True
|
|
79
|
+
return saw_pattern
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _tokens_are_cached_diff(tokens: tuple[str, ...]) -> bool:
|
|
83
|
+
kind = cached_diff_kind(tokens)
|
|
84
|
+
return kind == "owned"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cached_diff_kind(tokens: tuple[str, ...]) -> _CACHED_DIFF_KIND | None:
|
|
88
|
+
"""Classify one segment as an owned or execution-routed cached diff."""
|
|
89
|
+
|
|
90
|
+
stripped = executable_tokens(tokens)
|
|
91
|
+
if not stripped:
|
|
92
|
+
return None
|
|
93
|
+
command_name, command_index = _shell_segment_primary_command(list(stripped))
|
|
94
|
+
if command_name != "git" or command_index is None:
|
|
95
|
+
return None
|
|
96
|
+
args = stripped[command_index + 1 :]
|
|
97
|
+
index = 0
|
|
98
|
+
routed = False
|
|
99
|
+
while index < len(args):
|
|
100
|
+
token = args[index]
|
|
101
|
+
option_name = token.partition("=")[0]
|
|
102
|
+
if token in _GIT_GLOBAL_FLAG_OPTIONS:
|
|
103
|
+
index += 1
|
|
104
|
+
continue
|
|
105
|
+
if token == "-c" or (token.startswith("-c") and not token.startswith("-C")):
|
|
106
|
+
routed = True
|
|
107
|
+
if token == "-c":
|
|
108
|
+
if index + 1 >= len(args):
|
|
109
|
+
return None
|
|
110
|
+
index += 2
|
|
111
|
+
continue
|
|
112
|
+
index += 1
|
|
113
|
+
continue
|
|
114
|
+
if option_name in _GIT_GLOBAL_VALUE_OPTIONS:
|
|
115
|
+
if option_name in _EXECUTION_ROUTING_OPTIONS:
|
|
116
|
+
routed = True
|
|
117
|
+
if "=" in token:
|
|
118
|
+
if not token.partition("=")[2]:
|
|
119
|
+
return None
|
|
120
|
+
index += 1
|
|
121
|
+
continue
|
|
122
|
+
if index + 1 >= len(args):
|
|
123
|
+
return None
|
|
124
|
+
index += 2
|
|
125
|
+
continue
|
|
126
|
+
if token.startswith("-"):
|
|
127
|
+
index += 1
|
|
128
|
+
continue
|
|
129
|
+
if token != "diff":
|
|
130
|
+
return None
|
|
131
|
+
operands = args[index + 1 :]
|
|
132
|
+
options = operands[: operands.index("--")] if "--" in operands else operands
|
|
133
|
+
if not _INDEX_DIFF_FLAGS.intersection(options):
|
|
134
|
+
return None
|
|
135
|
+
return "routed" if routed else "owned"
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def command_has_execution_routed_git_cached_diff(segments: tuple[tuple[str, ...], ...]) -> bool:
|
|
140
|
+
"""Return whether any cached-diff segment overrides Git execution routing."""
|
|
141
|
+
|
|
142
|
+
return any(cached_diff_kind(tokens) == "routed" for tokens in segments)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def command_has_owned_git_cached_diff(segments: tuple[tuple[str, ...], ...]) -> bool:
|
|
146
|
+
"""Return whether the command contains a named cached-diff Guard can own."""
|
|
147
|
+
|
|
148
|
+
if command_has_execution_routed_git_cached_diff(segments):
|
|
149
|
+
return False
|
|
150
|
+
return any(cached_diff_kind(tokens) == "owned" for tokens in segments)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def is_low_risk_git_index_inspection(
|
|
154
|
+
command_text: str,
|
|
155
|
+
*,
|
|
156
|
+
cwd: Path | None,
|
|
157
|
+
home_dir: Path | None = None,
|
|
158
|
+
) -> bool:
|
|
159
|
+
"""Recognize a repo-bound staged-index inspection, including if/then scans."""
|
|
160
|
+
|
|
161
|
+
return index_inspection_execution_context(command_text, cwd=cwd, home_dir=home_dir) is not None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def index_inspection_execution_context(
|
|
165
|
+
command_text: str,
|
|
166
|
+
*,
|
|
167
|
+
cwd: Path | None,
|
|
168
|
+
home_dir: Path | None = None,
|
|
169
|
+
) -> ShellExecutionContext | None:
|
|
170
|
+
"""Return the modeled context when a staged-index scan is repo-bound."""
|
|
171
|
+
|
|
172
|
+
if cwd is None:
|
|
173
|
+
return None
|
|
174
|
+
try:
|
|
175
|
+
execution_cwd = cwd.resolve()
|
|
176
|
+
except (OSError, RuntimeError):
|
|
177
|
+
return None
|
|
178
|
+
context = model_shell_execution_context(
|
|
179
|
+
command_text,
|
|
180
|
+
cwd=execution_cwd,
|
|
181
|
+
workspace_root=execution_cwd,
|
|
182
|
+
home_dir=home_dir,
|
|
183
|
+
)
|
|
184
|
+
if not _context_is_low_risk_git_index_inspection(context, home_dir=home_dir):
|
|
185
|
+
return None
|
|
186
|
+
return context
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _segment_has_mutating_redirection(tokens: tuple[str, ...]) -> bool:
|
|
190
|
+
for token in tokens:
|
|
191
|
+
if token in {">", ">>", ">|", "1>", "1>>", "1>|"}:
|
|
192
|
+
return True
|
|
193
|
+
if ">" in token and not token.startswith("--"):
|
|
194
|
+
return True
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _flow_controls(controls: tuple[str, ...]) -> tuple[str, ...]:
|
|
199
|
+
return tuple(control for control in controls if control != "\n")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _context_is_low_risk_git_index_inspection(
|
|
203
|
+
context: ShellExecutionContext,
|
|
204
|
+
*,
|
|
205
|
+
home_dir: Path | None,
|
|
206
|
+
) -> bool:
|
|
207
|
+
if not context.segments or len(context.segments) > _MAX_SEGMENTS:
|
|
208
|
+
return False
|
|
209
|
+
if not context.complete and context.reason_code != "shell_cwd_unresolved_syntax":
|
|
210
|
+
return False
|
|
211
|
+
saw_cached_diff = False
|
|
212
|
+
previous_was_cached_diff = False
|
|
213
|
+
for segment in context.segments:
|
|
214
|
+
if any(control not in _ALLOWED_CONTROLS for control in (*segment.control_before, *segment.control_after)):
|
|
215
|
+
return False
|
|
216
|
+
tokens = executable_tokens(segment.tokens)
|
|
217
|
+
if tokens is None:
|
|
218
|
+
return False
|
|
219
|
+
if not tokens:
|
|
220
|
+
previous_was_cached_diff = False
|
|
221
|
+
continue
|
|
222
|
+
if _segment_has_mutating_redirection(segment.tokens):
|
|
223
|
+
return False
|
|
224
|
+
command_name, command_index = _shell_segment_primary_command(list(tokens))
|
|
225
|
+
if command_name is None or command_index is None:
|
|
226
|
+
return False
|
|
227
|
+
executable = tokens[command_index]
|
|
228
|
+
if "/" in executable or "\\" in executable:
|
|
229
|
+
return False
|
|
230
|
+
args = tokens[command_index + 1 :]
|
|
231
|
+
if command_name == "git":
|
|
232
|
+
if not _git_cached_diff_segment_is_safe(segment, tokens, home_dir=home_dir):
|
|
233
|
+
return False
|
|
234
|
+
saw_cached_diff = True
|
|
235
|
+
previous_was_cached_diff = True
|
|
236
|
+
continue
|
|
237
|
+
if command_name == "echo":
|
|
238
|
+
if not all(_static_echo_arg_is_safe(arg) for arg in args):
|
|
239
|
+
return False
|
|
240
|
+
previous_was_cached_diff = False
|
|
241
|
+
continue
|
|
242
|
+
if command_name == "rg":
|
|
243
|
+
if _flow_controls(segment.control_before) != ("|",) or not previous_was_cached_diff:
|
|
244
|
+
return False
|
|
245
|
+
if any(token.startswith("RIPGREP_CONFIG_PATH=") for token in segment.tokens) and "--no-config" not in args:
|
|
246
|
+
return False
|
|
247
|
+
if not _index_scan_rg_args_are_safe(args):
|
|
248
|
+
return False
|
|
249
|
+
previous_was_cached_diff = False
|
|
250
|
+
continue
|
|
251
|
+
return False
|
|
252
|
+
return saw_cached_diff
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def owned_git_index_inspection_action_class(
|
|
256
|
+
command_text: str,
|
|
257
|
+
*,
|
|
258
|
+
cwd: Path | None,
|
|
259
|
+
home_dir: Path | None,
|
|
260
|
+
) -> str | None:
|
|
261
|
+
"""Return the Git-protection action class for an unproven cached diff."""
|
|
262
|
+
|
|
263
|
+
if "git" not in command_text or "diff" not in command_text or "--cached" not in command_text:
|
|
264
|
+
return None
|
|
265
|
+
parsing_cwd = cwd or home_dir or Path.cwd()
|
|
266
|
+
context = model_shell_execution_context(
|
|
267
|
+
command_text,
|
|
268
|
+
cwd=parsing_cwd,
|
|
269
|
+
workspace_root=parsing_cwd,
|
|
270
|
+
home_dir=home_dir,
|
|
271
|
+
)
|
|
272
|
+
segment_tokens = tuple(segment.tokens for segment in context.segments)
|
|
273
|
+
if command_has_execution_routed_git_cached_diff(segment_tokens):
|
|
274
|
+
return None
|
|
275
|
+
if not command_has_owned_git_cached_diff(segment_tokens):
|
|
276
|
+
return None
|
|
277
|
+
if cwd is not None and is_low_risk_git_index_inspection(command_text, cwd=cwd, home_dir=home_dir):
|
|
278
|
+
return None
|
|
279
|
+
if cwd is not None and is_low_risk_standalone_git_routine(context):
|
|
280
|
+
return None
|
|
281
|
+
return "git index inspection"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
_SAFE_CACHED_DIFF_FLAGS: Final = frozenset(
|
|
285
|
+
{"--cached", "--check", "--name-only", "--name-status", "--staged", "--stat", "HEAD"}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _safe_exclude_pathspec(value: str) -> bool:
|
|
290
|
+
if not value.startswith((":!", ":^")):
|
|
291
|
+
return False
|
|
292
|
+
remainder = value[2:]
|
|
293
|
+
if not remainder or remainder.startswith((":", "/", "~")):
|
|
294
|
+
return False
|
|
295
|
+
return _safe_repository_path(remainder)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _cached_diff_operands_are_safe(args: tuple[str, ...]) -> bool:
|
|
299
|
+
if len(args) > 20:
|
|
300
|
+
return False
|
|
301
|
+
if "--" not in args:
|
|
302
|
+
return bool(_INDEX_DIFF_FLAGS.intersection(args)) and all(
|
|
303
|
+
arg in _SAFE_CACHED_DIFF_FLAGS or arg == "--cached" for arg in args
|
|
304
|
+
)
|
|
305
|
+
separator = args.index("--")
|
|
306
|
+
revisions = args[:separator]
|
|
307
|
+
if not _INDEX_DIFF_FLAGS.intersection(revisions):
|
|
308
|
+
return False
|
|
309
|
+
paths = args[separator + 1 :]
|
|
310
|
+
if not paths or len(paths) > 16:
|
|
311
|
+
return False
|
|
312
|
+
if any(arg not in _SAFE_CACHED_DIFF_FLAGS for arg in revisions):
|
|
313
|
+
return False
|
|
314
|
+
return all(_safe_repository_path(path) or _safe_exclude_pathspec(path) for path in paths)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _proof_cached_diff_tokens(tokens: tuple[str, ...]) -> tuple[str, ...]:
|
|
318
|
+
command_name, command_index = _shell_segment_primary_command(list(tokens))
|
|
319
|
+
if command_name != "git" or command_index is None:
|
|
320
|
+
return ("git", "diff", "--cached", "--check")
|
|
321
|
+
prefix = tokens[: command_index + 1]
|
|
322
|
+
args = tokens[command_index + 1 :]
|
|
323
|
+
if args[:1] == ("-C",) and len(args) >= 2:
|
|
324
|
+
return (*prefix, "-C", args[1], "diff", "--cached", "--check")
|
|
325
|
+
return (*prefix, "diff", "--cached", "--check")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _args_have_repository_selector(args: tuple[str, ...]) -> bool:
|
|
329
|
+
index = 0
|
|
330
|
+
while index < len(args):
|
|
331
|
+
token = args[index]
|
|
332
|
+
option_name = token.partition("=")[0]
|
|
333
|
+
if token == "-C" or token.startswith("-C") or option_name in _REPOSITORY_SELECTOR_OPTIONS:
|
|
334
|
+
return True
|
|
335
|
+
if token in _GIT_GLOBAL_FLAG_OPTIONS:
|
|
336
|
+
index += 1
|
|
337
|
+
continue
|
|
338
|
+
if option_name in _GIT_GLOBAL_VALUE_OPTIONS:
|
|
339
|
+
index += 1 if "=" in token else 2
|
|
340
|
+
continue
|
|
341
|
+
if token.startswith("-"):
|
|
342
|
+
index += 1
|
|
343
|
+
continue
|
|
344
|
+
return False
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _has_unproven_repository_selector(tokens: tuple[str, ...]) -> bool:
|
|
349
|
+
stripped = executable_tokens(tokens)
|
|
350
|
+
if not stripped:
|
|
351
|
+
return True
|
|
352
|
+
command_name, command_index = _shell_segment_primary_command(list(stripped))
|
|
353
|
+
if command_name != "git" or command_index is None:
|
|
354
|
+
return True
|
|
355
|
+
args = stripped[command_index + 1 :]
|
|
356
|
+
if args[:1] == ("-C",):
|
|
357
|
+
if len(args) < 2 or not args[1] or args[1].startswith("-"):
|
|
358
|
+
return True
|
|
359
|
+
return _args_have_repository_selector(args[2:])
|
|
360
|
+
return _args_have_repository_selector(args)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _git_cached_diff_segment_is_safe(
|
|
364
|
+
segment: ShellExecutionSegment,
|
|
365
|
+
tokens: tuple[str, ...],
|
|
366
|
+
*,
|
|
367
|
+
home_dir: Path | None,
|
|
368
|
+
) -> bool:
|
|
369
|
+
if not _tokens_are_cached_diff(tokens) or _has_unproven_repository_selector(tokens):
|
|
370
|
+
return False
|
|
371
|
+
operands = git_diff_operands(tokens)
|
|
372
|
+
if operands is None or not _cached_diff_operands_are_safe(operands):
|
|
373
|
+
return False
|
|
374
|
+
synthetic = replace(segment, tokens=_proof_cached_diff_tokens(tokens))
|
|
375
|
+
repository_path = tokens[2] if tokens[:2] == ("git", "-C") and len(tokens) > 2 else None
|
|
376
|
+
return is_low_risk_git_inspection_segment(synthetic) and _git_show_has_execution_free_config(
|
|
377
|
+
synthetic, repository_path=repository_path
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def git_diff_operands(tokens: tuple[str, ...]) -> tuple[str, ...] | None:
|
|
382
|
+
kind = cached_diff_kind(tokens)
|
|
383
|
+
if kind != "owned":
|
|
384
|
+
return None
|
|
385
|
+
stripped = executable_tokens(tokens)
|
|
386
|
+
if not stripped:
|
|
387
|
+
return None
|
|
388
|
+
command_name, command_index = _shell_segment_primary_command(list(stripped))
|
|
389
|
+
if command_name != "git" or command_index is None:
|
|
390
|
+
return None
|
|
391
|
+
args = stripped[command_index + 1 :]
|
|
392
|
+
index = 0
|
|
393
|
+
while index < len(args):
|
|
394
|
+
token = args[index]
|
|
395
|
+
option_name = token.partition("=")[0]
|
|
396
|
+
if token in _GIT_GLOBAL_FLAG_OPTIONS:
|
|
397
|
+
index += 1
|
|
398
|
+
continue
|
|
399
|
+
if option_name in _GIT_GLOBAL_VALUE_OPTIONS:
|
|
400
|
+
index += 1 if "=" in token else 2
|
|
401
|
+
continue
|
|
402
|
+
if token.startswith("-"):
|
|
403
|
+
index += 1
|
|
404
|
+
continue
|
|
405
|
+
if token != "diff":
|
|
406
|
+
return None
|
|
407
|
+
return args[index + 1 :]
|
|
408
|
+
return None
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
from ..compound_git_inspection import is_low_risk_git_inspection_segment
|
|
8
8
|
from ..git_execution_safety import trusted_git_binary_for_cwd
|
|
9
|
+
from ..git_index_inspection import is_low_risk_git_index_inspection
|
|
9
10
|
from ..kubernetes_commands import kubernetes_read_only_inventory_args
|
|
10
11
|
from ..shell_command_wrappers import is_trusted_absolute_command_path
|
|
11
12
|
from ..shell_execution_context import ShellExecutionContext, ShellExecutionSegment, model_shell_execution_context
|
|
@@ -75,7 +76,7 @@ def _looks_like_safe_compound_developer_inspection(
|
|
|
75
76
|
|
|
76
77
|
graph = _compound_developer_effect_graph(command_text, cwd=cwd, home_dir=home_dir)
|
|
77
78
|
if graph is None or not graph.context.complete:
|
|
78
|
-
return
|
|
79
|
+
return is_low_risk_git_index_inspection(command_text, cwd=cwd, home_dir=home_dir)
|
|
79
80
|
silently_verified_effects = {
|
|
80
81
|
DeveloperShellEffect.DIRECTORY,
|
|
81
82
|
DeveloperShellEffect.LOCAL_READ,
|
|
@@ -151,7 +152,7 @@ def _git_segment_is_silently_verified(
|
|
|
151
152
|
return "|" not in (*segment.control_before, *segment.control_after) and is_low_risk_git_inspection_segment(
|
|
152
153
|
segment
|
|
153
154
|
)
|
|
154
|
-
if operation in {"blame", "branch", "show", "worktree"}:
|
|
155
|
+
if operation in {"blame", "branch", "diff", "show", "worktree"}:
|
|
155
156
|
return is_low_risk_git_inspection_segment(segment)
|
|
156
157
|
return operation in {"ls-files", "rev-parse"}
|
|
157
158
|
|
|
@@ -7,6 +7,7 @@ import re
|
|
|
7
7
|
from dataclasses import replace
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from ..git_index_inspection import index_inspection_execution_context
|
|
10
11
|
from ..shell_execution_context import ShellExecutionContext, model_shell_execution_context
|
|
11
12
|
from .developer_inspection import (
|
|
12
13
|
_low_risk_compound_developer_execution_context,
|
|
@@ -37,11 +38,15 @@ def low_risk_compound_developer_execution_context(
|
|
|
37
38
|
)
|
|
38
39
|
return replace(recovered, command_text=command_text) if recovered is not None else None
|
|
39
40
|
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
return (
|
|
42
|
+
_low_risk_compound_developer_execution_context(
|
|
43
|
+
command_text,
|
|
44
|
+
cwd=cwd,
|
|
45
|
+
home_dir=home_dir,
|
|
46
|
+
)
|
|
47
|
+
or index_inspection_execution_context(command_text, cwd=cwd, home_dir=home_dir)
|
|
48
|
+
or _bounded_verified_source_edit_execution_context(command_text, home_dir=home_dir)
|
|
49
|
+
)
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
def _bounded_verified_source_edit_execution_context(
|
|
@@ -10,6 +10,7 @@ from typing import cast
|
|
|
10
10
|
from ..command_model import CanonicalCommand
|
|
11
11
|
from ..compound_git_inspection import is_low_risk_git_inspection_segment, is_low_risk_standalone_git_routine
|
|
12
12
|
from ..git_execution_safety import git_status_args_are_read_only, git_status_has_execution_free_config
|
|
13
|
+
from ..git_index_inspection import owned_git_index_inspection_action_class
|
|
13
14
|
from ..kubernetes_commands import kubernetes_secret_read_source
|
|
14
15
|
from ..shell_command_wrappers import normalize_transparent_shell_command
|
|
15
16
|
from ..shell_execution_context import ShellExecutionContext, model_shell_execution_context
|
|
@@ -266,7 +267,7 @@ def _unverified_git_fetch_request(
|
|
|
266
267
|
cwd: Path | None,
|
|
267
268
|
home_dir: Path | None,
|
|
268
269
|
) -> ToolActionRequestMatch | None:
|
|
269
|
-
if "git" not in command_text
|
|
270
|
+
if "git" not in command_text:
|
|
270
271
|
return None
|
|
271
272
|
parsing_cwd = cwd or home_dir or Path.cwd()
|
|
272
273
|
context = model_shell_execution_context(
|
|
@@ -276,7 +277,24 @@ def _unverified_git_fetch_request(
|
|
|
276
277
|
home_dir=home_dir,
|
|
277
278
|
)
|
|
278
279
|
if not any(_segment_invokes_git_fetch(segment.tokens) for segment in context.segments):
|
|
279
|
-
|
|
280
|
+
action_class = owned_git_index_inspection_action_class(
|
|
281
|
+
command_text,
|
|
282
|
+
cwd=cwd,
|
|
283
|
+
home_dir=home_dir,
|
|
284
|
+
)
|
|
285
|
+
if action_class is None:
|
|
286
|
+
return None
|
|
287
|
+
return ToolActionRequestMatch(
|
|
288
|
+
tool_name=tool_name,
|
|
289
|
+
normalized_tool_name=normalized_tool_name,
|
|
290
|
+
command_text=command_text,
|
|
291
|
+
action_class=action_class,
|
|
292
|
+
reason=(
|
|
293
|
+
"Git cached diff needs a repository-bound index inspection Guard can verify. "
|
|
294
|
+
"Use git diff --cached with check/stat flags or lockfile excludes, "
|
|
295
|
+
"or confirm the exact index read in Guard."
|
|
296
|
+
),
|
|
297
|
+
)
|
|
280
298
|
if cwd is not None and is_low_risk_standalone_git_routine(context):
|
|
281
299
|
return None
|
|
282
300
|
if context.complete and all(
|
codex_plugin_scanner/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plugin-scanner
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.113
|
|
4
4
|
Summary: Lint, verify, and gate plugin ecosystems for maintainers, CI, and publish workflows.
|
|
5
5
|
Project-URL: Homepage, https://hol.org/guard
|
|
6
6
|
Project-URL: Security, https://hol.org/guard/security
|
|
@@ -28,7 +28,7 @@ codex_plugin_scanner/trust_scoring.py,sha256=t_Lw13HZ766UCJzTtNNwllZK_k2ks_x-8St
|
|
|
28
28
|
codex_plugin_scanner/trust_skill_scoring.py,sha256=e-SrMBgAruzFn4RD0pGpP9rtPXxc2ji_CKpcnFPRh3M,12542
|
|
29
29
|
codex_plugin_scanner/trust_specs.py,sha256=aYGSB8M9RQp1uwhsRaWlVa1M9Vk51kUIxmBXkW8noZU,10729
|
|
30
30
|
codex_plugin_scanner/verification.py,sha256=J2nsnYlxNYecSvmSQhV_leImLslCOotIK2FSmEoIjg0,27896
|
|
31
|
-
codex_plugin_scanner/version.py,sha256=
|
|
31
|
+
codex_plugin_scanner/version.py,sha256=KgFbHTUEzOtocytyfrjipxG2nB-W_SCggizSg8V1BW0,72
|
|
32
32
|
codex_plugin_scanner/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
33
|
codex_plugin_scanner/checks/best_practices.py,sha256=uB3DXLggqxjfyBMH_1yvX4nyFhpIvaRHjTyyueoO-GQ,8473
|
|
34
34
|
codex_plugin_scanner/checks/claude.py,sha256=cMucH5pmKgQtBnDcycOzMMlKL-Q5HQemPqUmTLF0DzE,10189
|
|
@@ -462,8 +462,8 @@ codex_plugin_scanner/guard/runtime/command_activity_lifecycle.py,sha256=N6MZQsCH
|
|
|
462
462
|
codex_plugin_scanner/guard/runtime/command_activity_privacy.py,sha256=X9ytuqSTDtIDRlo294MbImLVlUjAnl5-O_FnaaXqfaM,12695
|
|
463
463
|
codex_plugin_scanner/guard/runtime/command_backup_extensions.py,sha256=9lX8La9_YIicQ8J76iBC9fPGsJhxhH4YzBuzamY0GLU,10105
|
|
464
464
|
codex_plugin_scanner/guard/runtime/command_builtin_extension_catalog.py,sha256=Yogu6iiOCWQU7DaPr4gfa8gOsTmu-HJns5c2DsaZUrE,2200
|
|
465
|
-
codex_plugin_scanner/guard/runtime/command_builtin_extension_registry.py,sha256=
|
|
466
|
-
codex_plugin_scanner/guard/runtime/command_builtin_rules.py,sha256
|
|
465
|
+
codex_plugin_scanner/guard/runtime/command_builtin_extension_registry.py,sha256=2gXikIL5m0iO_CNRkZUgb5UMnLKcucX0wFEXySgyZqw,8139
|
|
466
|
+
codex_plugin_scanner/guard/runtime/command_builtin_rules.py,sha256=-sWYS27t0kmE7vRDrnic_hZAnq4O4iMyQQ8wGGsaVXU,20185
|
|
467
467
|
codex_plugin_scanner/guard/runtime/command_capability.py,sha256=12xyfo2T8YYqnX9NCcKE3eNdKKgRvyJRmgUKjZeh9Fw,25185
|
|
468
468
|
codex_plugin_scanner/guard/runtime/command_cicd_extensions.py,sha256=QSyP5EP0pdeAKsTf3cZL_mEX3fyihG6KazoD7TdFEJc,6819
|
|
469
469
|
codex_plugin_scanner/guard/runtime/command_cloud_extensions.py,sha256=-C9NJvqRXbIBZa11t-hh4qHwYkgjinKCoVomsHy1UEU,9601
|
|
@@ -506,7 +506,7 @@ codex_plugin_scanner/guard/runtime/command_tokens.py,sha256=GY9ehRZB8O_CGHxGh3xh
|
|
|
506
506
|
codex_plugin_scanner/guard/runtime/command_verified_read_candidates.py,sha256=Qh_0bMm3VCp6GUkEeagcLtwYvtO0X1A_HN9_lgU-6eI,6599
|
|
507
507
|
codex_plugin_scanner/guard/runtime/command_workspace_write_candidates.py,sha256=AANBnFfGC-Jzg3RL6nYkamF3ndn7obB6LhKuaH1DdaE,6633
|
|
508
508
|
codex_plugin_scanner/guard/runtime/composition_rules.py,sha256=5oo0b1kL6hmFYyMSD9kCKcgA-m6fDKWy4pDeq3i1Ajk,6797
|
|
509
|
-
codex_plugin_scanner/guard/runtime/compound_git_inspection.py,sha256=
|
|
509
|
+
codex_plugin_scanner/guard/runtime/compound_git_inspection.py,sha256=XxQ_pqurgsYN621nEEW2RPEZgT1iS8QRVUevZxU9t6A,17787
|
|
510
510
|
codex_plugin_scanner/guard/runtime/containment_contract.py,sha256=8UN1_-Qmr8i3VmBGddlxz3Vt7Pr3q-HHQBC10hBumq0,17963
|
|
511
511
|
codex_plugin_scanner/guard/runtime/containment_executor.py,sha256=RYQ2GnxRy3kHPyxTYaVEYjwIL__fMDzL5zsF2EPUHck,16454
|
|
512
512
|
codex_plugin_scanner/guard/runtime/containment_health.py,sha256=Ji0wdiLjACZL0f-r5JHtOenoXEFv1JkNIELrA0iALkM,13292
|
|
@@ -530,6 +530,7 @@ codex_plugin_scanner/guard/runtime/extension_control_resolver.py,sha256=aNmVrVjt
|
|
|
530
530
|
codex_plugin_scanner/guard/runtime/extension_evidence.py,sha256=6gS05shn1OI5bmuFxGu5El4rHuICsvpI65WJV7oezmY,10583
|
|
531
531
|
codex_plugin_scanner/guard/runtime/false_positive_rules.py,sha256=OcXN1FItvuapKosg7kRNiys9YcJN4ALVzN2hKdX4VCw,24316
|
|
532
532
|
codex_plugin_scanner/guard/runtime/git_execution_safety.py,sha256=D2Ma5jnvTFosfoTt9hmN2c95ZZGLOc1NTxFR9E_57HY,28722
|
|
533
|
+
codex_plugin_scanner/guard/runtime/git_index_inspection.py,sha256=XlQwb_cFmdjfWoTTC3nGzJfjVEt3BIb9Gyt7_q1EQqY,14467
|
|
533
534
|
codex_plugin_scanner/guard/runtime/git_pathspecs.py,sha256=skNNsz-60pnFUqsaYJI9RRkwzUGGttCF-fvMdpOq7iE,12785
|
|
534
535
|
codex_plugin_scanner/guard/runtime/github_actions_read_workflow.py,sha256=-SFO8GLLBFw1iURh9TePVFCid1DCJPzKkzx45u92VCY,16904
|
|
535
536
|
codex_plugin_scanner/guard/runtime/github_capability_contract.py,sha256=c7OLPdG6X_naoOrq-XpFyeoB2i0BvBzj_ynYPTjcILE,13091
|
|
@@ -650,7 +651,7 @@ codex_plugin_scanner/guard/runtime/secret_file_request_services/constants_patter
|
|
|
650
651
|
codex_plugin_scanner/guard/runtime/secret_file_request_services/credential_exfiltration.py,sha256=y-knVxmSJyMTcgsmnbzBbcbaInmHFIJuejAvBlylLyA,12911
|
|
651
652
|
codex_plugin_scanner/guard/runtime/secret_file_request_services/destructive_shell_detection.py,sha256=-KO2-JEBIn820jHxdwe0xiSI5EZPtQ8aj6MM13Nz33s,12148
|
|
652
653
|
codex_plugin_scanner/guard/runtime/secret_file_request_services/developer_inspection.py,sha256=3YImzaN_7G020orTLn6TQ4xoavSgPkdX5D_mzi29z7c,22430
|
|
653
|
-
codex_plugin_scanner/guard/runtime/secret_file_request_services/developer_routines.py,sha256=
|
|
654
|
+
codex_plugin_scanner/guard/runtime/secret_file_request_services/developer_routines.py,sha256=UnufG04UyQ7xbwhxZHdzofIwjKlbYUjOPHiewqgBa4k,11240
|
|
654
655
|
codex_plugin_scanner/guard/runtime/secret_file_request_services/docker_requests.py,sha256=c-8PEQVOG6KbryJaDKZQw9wCIlaniV_ULKPG48BC5Eg,15308
|
|
655
656
|
codex_plugin_scanner/guard/runtime/secret_file_request_services/encoded_payloads.py,sha256=kiEG_GxK1Rwhji3G8mZE0VZBQ32RDqHoEEIqGgLJI7A,16106
|
|
656
657
|
codex_plugin_scanner/guard/runtime/secret_file_request_services/git_routines.py,sha256=xsfuu3TPK8Ox1lFUDukN3xHG838-8Bm7FTJgbRChjWI,10335
|
|
@@ -684,8 +685,8 @@ codex_plugin_scanner/guard/runtime/secret_file_request_services/shell_request_cl
|
|
|
684
685
|
codex_plugin_scanner/guard/runtime/secret_file_request_services/shell_static_safety.py,sha256=h1ew2l9euL5fIQYn2hj8b0PQiM7CUvHGR-WsS-gJ4WA,15237
|
|
685
686
|
codex_plugin_scanner/guard/runtime/secret_file_request_services/shell_stdin_sources.py,sha256=dvGdc8dJvYjFF29fdQNtixuyQsjUEXo54cx7Pf_RN_g,9887
|
|
686
687
|
codex_plugin_scanner/guard/runtime/secret_file_request_services/shell_tokenization.py,sha256=2beHCIfbvsPoF2qfqrZ5rpgpJORFf1qeSH9JNPsZTCo,12861
|
|
687
|
-
codex_plugin_scanner/guard/runtime/secret_file_request_services/source_edit_context.py,sha256=
|
|
688
|
-
codex_plugin_scanner/guard/runtime/secret_file_request_services/tool_action_requests.py,sha256=
|
|
688
|
+
codex_plugin_scanner/guard/runtime/secret_file_request_services/source_edit_context.py,sha256=iV5Z2PtenR8nWixOta4nxOYcxGYqxGRdupi-x5JjvBw,11067
|
|
689
|
+
codex_plugin_scanner/guard/runtime/secret_file_request_services/tool_action_requests.py,sha256=Q8LUU6SNM8z6tCrX-AYmCwnH_7blbTJ7ypWM5M4vcVk,17352
|
|
689
690
|
codex_plugin_scanner/guard/runtime/secret_file_request_services/typescript_graphql_safety.py,sha256=OFj97zDy-WjVgbVu76eONG315Z1rPUMapySb-9b52RU,13041
|
|
690
691
|
codex_plugin_scanner/guard/runtime/secret_file_request_services/upload_arguments.py,sha256=25ZGoYVG5kiCK1SChRt6OdyWuDc5M6CjZS6rSNW7k1U,10579
|
|
691
692
|
codex_plugin_scanner/guard/schemas/__init__.py,sha256=YFJmxLMTfLclVIQ7ngTwRkeRLdBJEAF_ljc2jJTgvjE,220
|
|
@@ -699,8 +700,8 @@ codex_plugin_scanner/integrations/cisco_skill_scanner.py,sha256=3tW9Ya5riLsIbU9Y
|
|
|
699
700
|
codex_plugin_scanner/rules/__init__.py,sha256=bhAnNlVvEiAoksf_-Vd4f74JNz66pTINhUO2mMc8kvs,293
|
|
700
701
|
codex_plugin_scanner/rules/registry.py,sha256=7BaRerM26teDUM0S48d3sPHbqzmq2JhuelC0_ni3364,4580
|
|
701
702
|
codex_plugin_scanner/rules/specs.py,sha256=gpU0lijxqdSEZMMf28vciQxob-lvBEPcMQ4BG6N_r3I,650
|
|
702
|
-
plugin_scanner-2.2.
|
|
703
|
-
plugin_scanner-2.2.
|
|
704
|
-
plugin_scanner-2.2.
|
|
705
|
-
plugin_scanner-2.2.
|
|
706
|
-
plugin_scanner-2.2.
|
|
703
|
+
plugin_scanner-2.2.113.dist-info/METADATA,sha256=fiqzYiXI-fXoa2tKa1TR-zZ9k33Zrj4y4t7qoMj1Jdg,47988
|
|
704
|
+
plugin_scanner-2.2.113.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
705
|
+
plugin_scanner-2.2.113.dist-info/entry_points.txt,sha256=SLGKi3GvvSsPwave-k2JQt2meG0pM7aaC6VlUy47Crg,122
|
|
706
|
+
plugin_scanner-2.2.113.dist-info/licenses/LICENSE,sha256=UbiP0-niTtzc4d0UWk_KLc0SkRQH8AsG36b_YA-XM8E,11358
|
|
707
|
+
plugin_scanner-2.2.113.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|