plugin-scanner 2.2.111__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.
@@ -1418,7 +1418,11 @@ def _build_package_protect_authority(
1418
1418
  guard_home=store.guard_home,
1419
1419
  launch_cwd=launch_cwd,
1420
1420
  )
1421
- intent = _package_intent_parser_module().parse_package_intent(shlex.join(command), workspace=launch_cwd)
1421
+ intent = _package_intent_parser_module().parse_package_intent(
1422
+ shlex.join(command),
1423
+ workspace=launch_cwd,
1424
+ environment=launch_environment,
1425
+ )
1422
1426
  if intent is None:
1423
1427
  return None
1424
1428
  sanitized_intent = replace(intent, redacted_command=shlex.join(redacted_command_tokens(command)))
@@ -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="Reviews local and remote Git operations that can discard work or replace history.",
57
- action_classes=("git destructive command",),
58
- risk_classes=("destructive_shell",),
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
@@ -10,7 +10,7 @@ import re
10
10
  import shlex
11
11
  import shutil
12
12
  import stat
13
- from collections.abc import Iterable
13
+ from collections.abc import Iterable, Mapping
14
14
  from dataclasses import dataclass, replace
15
15
  from pathlib import Path
16
16
 
@@ -61,6 +61,7 @@ _CONTROL_CONTEXT_LABELS = {
61
61
  }
62
62
  _EXECUTION_CONTEXT_HMAC_KEY = os.urandom(32)
63
63
  _ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$")
64
+ _ENV_REFERENCE_RE = re.compile(r"\$(?:\{(?P<braced>[A-Za-z_][A-Za-z0-9_]*)\}|(?P<plain>[A-Za-z_][A-Za-z0-9_]*))")
64
65
  _LOCAL_EXECUTION_COMMANDS = frozenset({"bunx", "npx"})
65
66
  _PACKAGE_COMMAND_NAMES = frozenset(
66
67
  {
@@ -139,6 +140,7 @@ def parse_package_intent(
139
140
  workspace: Path | None = None,
140
141
  home_dir: Path | None = None,
141
142
  canonical_command: CanonicalCommand | None = None,
143
+ environment: Mapping[str, str] | None = None,
142
144
  ) -> PackageIntent | None:
143
145
  handlers = {
144
146
  "npm": _parse_npm_intent,
@@ -175,7 +177,12 @@ def parse_package_intent(
175
177
  "helm": _parse_helm_intent,
176
178
  }
177
179
  intents: list[PackageIntent] = []
178
- for segment in _normalized_command_segments(command_text, workspace=workspace, home_dir=home_dir):
180
+ for segment in _normalized_command_segments(
181
+ command_text,
182
+ workspace=workspace,
183
+ home_dir=home_dir,
184
+ environment=environment,
185
+ ):
179
186
  if not segment.tokens:
180
187
  continue
181
188
  command_name = _command_name(segment.tokens[0])
@@ -1191,7 +1198,9 @@ def _normalized_command_segments(
1191
1198
  *,
1192
1199
  workspace: Path | None = None,
1193
1200
  home_dir: Path | None = None,
1201
+ environment: Mapping[str, str] | None = None,
1194
1202
  ) -> tuple[_CommandSegment, ...]:
1203
+ inherited_environment = environment if environment is not None else os.environ
1195
1204
  execution_context = model_shell_execution_context(
1196
1205
  command_text,
1197
1206
  cwd=workspace,
@@ -1241,6 +1250,7 @@ def _normalized_command_segments(
1241
1250
  workspace=workspace,
1242
1251
  initial_cwd=modeled_cwd,
1243
1252
  initial_cwd_source=context_segment.cwd_source,
1253
+ environment=inherited_environment,
1244
1254
  )
1245
1255
  context_complete = validation_reason is None and context_segment.complete
1246
1256
  segments.append(
@@ -1431,10 +1441,12 @@ def _effective_execution_context(
1431
1441
  workspace: Path | None,
1432
1442
  initial_cwd: Path | None = None,
1433
1443
  initial_cwd_source: str | None = None,
1444
+ environment: Mapping[str, str] | None = None,
1434
1445
  ) -> tuple[str | None, str, Path, str]:
1446
+ supplied_environment = environment if environment is not None else os.environ
1435
1447
  effective_cwd = (initial_cwd or workspace or Path.cwd()).expanduser().resolve()
1436
1448
  cwd_source = initial_cwd_source or ("workspace" if workspace is not None else "process")
1437
- effective_path = os.environ.get("PATH")
1449
+ effective_path = supplied_environment.get("PATH")
1438
1450
  path_source = "inherited" if effective_path is not None else "inherited_unset"
1439
1451
  index, effective_path, path_source = _consume_path_assignments(
1440
1452
  raw_segment,
@@ -1442,6 +1454,7 @@ def _effective_execution_context(
1442
1454
  effective_path,
1443
1455
  path_source,
1444
1456
  direct_source="inline",
1457
+ environment=supplied_environment,
1445
1458
  )
1446
1459
  if index >= len(raw_segment):
1447
1460
  return _path_for_resolution(effective_path, effective_cwd), path_source, effective_cwd, cwd_source
@@ -1465,6 +1478,7 @@ def _effective_execution_context(
1465
1478
  effective_path,
1466
1479
  path_source,
1467
1480
  direct_source="inline",
1481
+ environment=supplied_environment,
1468
1482
  )
1469
1483
  if index >= len(raw_segment):
1470
1484
  return _path_for_resolution(effective_path, effective_cwd), path_source, effective_cwd, cwd_source
@@ -1472,7 +1486,7 @@ def _effective_execution_context(
1472
1486
  if command_name != "env":
1473
1487
  return _path_for_resolution(effective_path, effective_cwd), path_source, effective_cwd, cwd_source
1474
1488
 
1475
- inherited_environment = dict(os.environ)
1489
+ inherited_environment = dict(supplied_environment)
1476
1490
  if effective_path is None:
1477
1491
  inherited_environment.pop("PATH", None)
1478
1492
  else:
@@ -1490,10 +1504,18 @@ def _effective_execution_context(
1490
1504
  None,
1491
1505
  )
1492
1506
  if parsed_env.option_effects.search_path is not None:
1493
- effective_path = _expanded_path_assignment(parsed_env.option_effects.search_path, effective_path)
1507
+ effective_path = _expanded_path_assignment(
1508
+ parsed_env.option_effects.search_path,
1509
+ effective_path,
1510
+ environment=inherited_environment,
1511
+ )
1494
1512
  path_source = "env_search_path" if effective_path is not None else "env_search_path_unresolved"
1495
1513
  elif path_assignment is not None:
1496
- effective_path = _expanded_path_assignment(path_assignment, effective_path)
1514
+ effective_path = _expanded_path_assignment(
1515
+ path_assignment,
1516
+ effective_path,
1517
+ environment=inherited_environment,
1518
+ )
1497
1519
  path_source = "env" if effective_path is not None else "env_unresolved"
1498
1520
  elif parsed_env.option_effects.ignore_environment or path_was_unset:
1499
1521
  effective_path = os.defpath
@@ -1517,12 +1539,13 @@ def _consume_path_assignments(
1517
1539
  current_source: str,
1518
1540
  *,
1519
1541
  direct_source: str,
1542
+ environment: Mapping[str, str],
1520
1543
  ) -> tuple[int, str | None, str]:
1521
1544
  path_source = current_source
1522
1545
  while index < len(tokens) and _ENV_ASSIGNMENT_RE.match(tokens[index]):
1523
1546
  name, _, value = tokens[index].partition("=")
1524
1547
  if name == "PATH":
1525
- current_path = _expanded_path_assignment(value, current_path)
1548
+ current_path = _expanded_path_assignment(value, current_path, environment=environment)
1526
1549
  path_source = direct_source if current_path is not None else f"{direct_source}_unresolved"
1527
1550
  index += 1
1528
1551
  return index, current_path, path_source
@@ -1553,9 +1576,20 @@ def _updated_effective_cwd(current_cwd: Path, value: str) -> tuple[Path, str]:
1553
1576
  return current_cwd, "env_chdir_unresolved"
1554
1577
 
1555
1578
 
1556
- def _expanded_path_assignment(value: str, current_path: str | None) -> str | None:
1557
- expanded = value.replace("${PATH}", current_path or "").replace("$PATH", current_path or "")
1558
- expanded = os.path.expandvars(expanded)
1579
+ def _expanded_path_assignment(
1580
+ value: str,
1581
+ current_path: str | None,
1582
+ *,
1583
+ environment: Mapping[str, str],
1584
+ ) -> str | None:
1585
+ expansion_environment = dict(environment)
1586
+ expansion_environment["PATH"] = current_path or ""
1587
+
1588
+ def replace_reference(match: re.Match[str]) -> str:
1589
+ name = match.group("braced") or match.group("plain")
1590
+ return expansion_environment.get(name, match.group(0))
1591
+
1592
+ expanded = _ENV_REFERENCE_RE.sub(replace_reference, value)
1559
1593
  if "$" in expanded or "\x00" in expanded:
1560
1594
  return None
1561
1595
  return expanded
@@ -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 False
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 _low_risk_compound_developer_execution_context(
41
- command_text,
42
- cwd=cwd,
43
- home_dir=home_dir,
44
- ) or _bounded_verified_source_edit_execution_context(command_text, home_dir=home_dir)
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 or "fetch" 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
- return None
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(
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for tool version."""
2
2
 
3
- __version__ = "2.2.111"
3
+ __version__ = "2.2.113"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugin-scanner
3
- Version: 2.2.111
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=e2ZatvkDrTaf1ARU9WqTwI2Tlf2HJDngTRYJ2tE9ngc,72
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
@@ -112,7 +112,7 @@ codex_plugin_scanner/guard/inventory_item_description.py,sha256=vSSxbQLAYQHGE0oY
112
112
  codex_plugin_scanner/guard/launcher.py,sha256=2iD2taiyxGTdnWV-9PCIgVMJwEG7iowmE0K0Yt5mCC0,2533
113
113
  codex_plugin_scanner/guard/local_authority_integrity.py,sha256=dS_7IUhg__Ea5esUUklEL0ZBOgSTnqBcQ982V1bO6_Q,5832
114
114
  codex_plugin_scanner/guard/local_dashboard_session.py,sha256=LpSDyNjhblUyD589uQQUyFdsecpPmw2yWThgAKIjyHI,1722
115
- codex_plugin_scanner/guard/local_supply_chain.py,sha256=pOBbNgwCZsjgSdtSopb_McXlIQg5bhwOBgmiDKkuNhw,171701
115
+ codex_plugin_scanner/guard/local_supply_chain.py,sha256=6lRbkKbzFO6Fbu30RdMUIGb3nS3ySDuvdIwyS1YiqhY,171764
116
116
  codex_plugin_scanner/guard/local_trust_contract.py,sha256=F2J3KygnvXrGtuBJ9I96RBLJg-NFqxE3gQGSPx8n-3g,14903
117
117
  codex_plugin_scanner/guard/local_trust_controller.py,sha256=6AY-kf-Q1OjPE63F4La5uX6UQqGcWhYXKkyw23jSwiU,10853
118
118
  codex_plugin_scanner/guard/managed_install_proof.py,sha256=GecgR6J6jjnV0zZ25cAJvrTfJTycuVDbCJjFn7DeCVs,3343
@@ -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=0tszfUNg0Km4ROHTxbBbl-em2Aq0vDRZZR8pfzzEkIk,7923
466
- codex_plugin_scanner/guard/runtime/command_builtin_rules.py,sha256=FuaAdtRxdm1iwN0a1p0UiPKF__y3K_BlQFms9NF0Y18,19746
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=_KDl3nXLAs5X7-EpsijjUqOJDzXGaY0zmsuIMXxX3BM,17705
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
@@ -585,7 +586,7 @@ codex_plugin_scanner/guard/runtime/offline_archive_worker.py,sha256=mimHsG9u6O4K
585
586
  codex_plugin_scanner/guard/runtime/package_execution_policy.py,sha256=duV4_QIDmkb6JGXOIYK9ilgfd3R0gFE8I1ON89QTuEQ,555
586
587
  codex_plugin_scanner/guard/runtime/package_intent.py,sha256=rqiGU_XnLE-xk6gN6_eVRVIIYXSBGmrBzdjwnI95BWk,681
587
588
  codex_plugin_scanner/guard/runtime/package_intent_common.py,sha256=dZizvm7vKw978ntat-uR4OzQLu-3_a-TjzlYxLtVkFg,23883
588
- codex_plugin_scanner/guard/runtime/package_intent_parser.py,sha256=-RNnHw_RRmZO-hZzC9CFUFdWmvdUMysbQOxakeVOL6k,65069
589
+ codex_plugin_scanner/guard/runtime/package_intent_parser.py,sha256=VCh2qU0pGnghjD9AXvJWlhFxgDjSjm_aFzGKTGtWxcw,66214
589
590
  codex_plugin_scanner/guard/runtime/package_manager_command.py,sha256=KTxy8LFadce-GxprijT83_vxSrz2otiyoiPnvsA4dMg,5213
590
591
  codex_plugin_scanner/guard/runtime/package_manifest_diff.py,sha256=49pranXVP4qNdm0wJB232WUG_lckqeLSAn1EPEQXJpA,26327
591
592
  codex_plugin_scanner/guard/runtime/persistence_rules.py,sha256=SP8JPsL_B_ebICm2l9QG7btcblrb90VjaIeYN3WaYno,6587
@@ -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=eui3zTGogz-fyPRy_NBT3r4-i6Rm1NpPL2vbnn48ZRg,11095
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=uPHtXx2o-Wl4dQzI2qXp697lEh_zcVX302d4vDfjA7Q,10869
688
- codex_plugin_scanner/guard/runtime/secret_file_request_services/tool_action_requests.py,sha256=yOQD_b7YyXDmC1Gua_Qz7p1bCCnTC1X8NT9xNPwfsj0,16628
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.111.dist-info/METADATA,sha256=D-4utaCuLhWmbPskoNk_4CnHy4GHf0MlsXWry_20Vvo,47988
703
- plugin_scanner-2.2.111.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
704
- plugin_scanner-2.2.111.dist-info/entry_points.txt,sha256=SLGKi3GvvSsPwave-k2JQt2meG0pM7aaC6VlUy47Crg,122
705
- plugin_scanner-2.2.111.dist-info/licenses/LICENSE,sha256=UbiP0-niTtzc4d0UWk_KLc0SkRQH8AsG36b_YA-XM8E,11358
706
- plugin_scanner-2.2.111.dist-info/RECORD,,
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,,