@danmoisan/drm-copilot-mcp 1.1.17 → 1.1.19

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.
Files changed (225) hide show
  1. package/out/mcp-server.js +1405 -684
  2. package/package.json +6 -4
  3. package/resources/claude-customizations/.claude/agents/atomic-executor.md +11 -0
  4. package/resources/claude-customizations/.claude/agents/atomic-planner.md +10 -0
  5. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +12 -4
  6. package/resources/claude-customizations/.claude/agents/epic-planner.md +5 -4
  7. package/resources/claude-customizations/.claude/agents/feature-review.md +16 -5
  8. package/resources/claude-customizations/.claude/agents/orchestrator.md +21 -7
  9. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +37 -13
  10. package/resources/claude-customizations/.claude/agents/parallel-planner.md +9 -6
  11. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -2
  12. package/resources/claude-customizations/.claude/agents/prd-feature.md +1 -1
  13. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +7 -1
  14. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +7 -1
  15. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +7 -1
  16. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +111 -46
  17. package/resources/claude-customizations/.claude/hooks/enforce-completion-helpers.ps1 +105 -0
  18. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +8 -1
  19. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +7 -1
  20. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +116 -59
  21. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +55 -23
  22. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +74 -75
  23. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +127 -35
  24. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +61 -57
  25. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +7 -1
  26. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +129 -39
  27. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +7 -1
  28. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +9 -3
  29. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +39 -51
  30. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +31 -58
  31. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +63 -47
  32. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-targets.ps1 +433 -0
  33. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +17 -12
  34. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +9 -3
  35. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +65 -50
  36. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +49 -42
  37. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +54 -72
  38. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +8 -2
  39. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +279 -0
  40. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +129 -75
  41. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.artifact-root.ps1 +233 -0
  42. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +224 -0
  43. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +69 -24
  44. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +29 -18
  45. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +10 -4
  46. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +14 -5
  47. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +8 -2
  48. package/resources/claude-customizations/.claude/hooks/feature-folder-resolution.ps1 +373 -0
  49. package/resources/claude-customizations/.claude/hooks/feature-review-coverage-thresholds.ps1 +85 -0
  50. package/resources/claude-customizations/.claude/hooks/hook-command-heredoc.ps1 +119 -0
  51. package/resources/claude-customizations/.claude/hooks/hook-command-invocation-operands.ps1 +198 -0
  52. package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +254 -255
  53. package/resources/claude-customizations/.claude/hooks/hook-command-payload-powershell.ps1 +201 -0
  54. package/resources/claude-customizations/.claude/hooks/hook-command-payload.ps1 +484 -0
  55. package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +27 -110
  56. package/resources/claude-customizations/.claude/hooks/hook-dependency-guard.ps1 +114 -0
  57. package/resources/claude-customizations/.claude/hooks/hook-docs-only-diff.ps1 +493 -0
  58. package/resources/claude-customizations/.claude/hooks/subagentstop-envelope.ps1 +192 -0
  59. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +9 -3
  60. package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +40 -42
  61. package/resources/claude-customizations/.claude/hooks/validate-executor-output.ps1 +66 -38
  62. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-metrics.ps1 +180 -0
  63. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-rules.ps1 +195 -0
  64. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage.ps1 +141 -395
  65. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-resolution.ps1 +311 -0
  66. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-routing.ps1 +170 -0
  67. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +115 -190
  68. package/resources/claude-customizations/.claude/hooks/validate-planner-output.ps1 +64 -32
  69. package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +39 -44
  70. package/resources/claude-customizations/.claude/hooks/validate-prd-feature-output.ps1 +43 -14
  71. package/resources/claude-customizations/.claude/hooks/validate-required-artifact-output.ps1 +66 -36
  72. package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +31 -34
  73. package/resources/claude-customizations/.claude/lib/bash/mutate-parallel-run.sh +191 -0
  74. package/resources/claude-customizations/.claude/lib/bash/parallel-cohorts.sh +13 -3
  75. package/resources/claude-customizations/.claude/lib/bash/parallel-lane-assertion.sh +4 -2
  76. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-cli.sh +152 -0
  77. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-ops.sh +193 -0
  78. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation.sh +322 -0
  79. package/resources/claude-customizations/.claude/lib/bash/remove-parallel-item.sh +146 -0
  80. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +23 -33
  81. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +86 -6
  82. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +91 -19
  83. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -13
  84. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateEpicWaveBarrier.psm1 +346 -0
  85. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +396 -0
  86. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +22 -15
  87. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +337 -0
  88. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +9 -1
  89. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +79 -2
  90. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +3 -3
  91. package/resources/claude-customizations/.claude/rules/architecture-boundaries.md +5 -5
  92. package/resources/claude-customizations/.claude/rules/csharp.md +2 -2
  93. package/resources/claude-customizations/.claude/rules/general-code-change.md +1 -1
  94. package/resources/claude-customizations/.claude/rules/general-unit-test.md +3 -2
  95. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +153 -4
  96. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +50 -5
  97. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +47 -8
  98. package/resources/claude-customizations/.claude/rules/quality-tiers.md +11 -7
  99. package/resources/claude-customizations/.claude/rules/shell.md +10 -8
  100. package/resources/claude-customizations/.claude/rules/typescript.md +1 -1
  101. package/resources/claude-customizations/.claude/settings.json +3 -4
  102. package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +9 -1
  103. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +7 -1
  104. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +11 -3
  105. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +11 -2
  106. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +1 -1
  107. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +171 -5
  108. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +1 -1
  109. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +6 -43
  110. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +6 -15
  111. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +45 -11
  112. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +1 -1
  113. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  114. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +10 -0
  115. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +7 -4
  116. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +42 -16
  117. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +52 -14
  118. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +22 -9
  119. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +97 -19
  120. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +63 -7
  121. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +35 -12
  122. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +3 -1
  123. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +1 -1
  124. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +7 -3
  125. package/resources/claude-customizations/pack-manifests/core.json +25 -0
  126. package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +9 -1
  127. package/resources/codex-and-agents-customizations/.agents/skills/architecture-boundaries/SKILL.md +5 -5
  128. package/resources/codex-and-agents-customizations/.agents/skills/atomic-executor/SKILL.md +11 -0
  129. package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +4 -0
  130. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +10 -0
  131. package/resources/codex-and-agents-customizations/.agents/skills/csharp/SKILL.md +2 -2
  132. package/resources/codex-and-agents-customizations/.agents/skills/csharp-qa-gate/SKILL.md +2 -2
  133. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +13 -3
  134. package/resources/codex-and-agents-customizations/.agents/skills/epic-plan/SKILL.md +17 -1
  135. package/resources/codex-and-agents-customizations/.agents/skills/epic-run/SKILL.md +2 -1
  136. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  137. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  138. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +30 -6
  139. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +16 -6
  140. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +1 -1
  141. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +3 -2
  142. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +43 -11
  143. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -2
  144. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +19 -4
  145. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +11 -7
  146. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +2 -0
  147. package/resources/codex-and-agents-customizations/.agents/skills/translate-claude-to-codex/SKILL.md +1 -1
  148. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +2 -2
  149. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +2 -2
  150. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +11 -0
  151. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +11 -0
  152. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +11 -0
  153. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +11 -0
  154. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +11 -0
  155. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +11 -0
  156. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +10 -0
  157. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +10 -0
  158. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +10 -0
  159. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +10 -0
  160. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +10 -0
  161. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +10 -0
  162. package/resources/codex-and-agents-customizations/.codex/agents/epic-orchestrator.toml +2 -1
  163. package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +6 -6
  164. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -0
  165. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -0
  166. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -0
  167. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -0
  168. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -0
  169. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -0
  170. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +3 -3
  171. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +3 -3
  172. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +3 -3
  173. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +3 -3
  174. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +3 -3
  175. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +3 -3
  176. package/resources/codex-and-agents-customizations/.codex/codex-web-setup.sh +318 -273
  177. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  178. package/resources/codex-and-agents-customizations/.codex/hooks/check-powershell-test-purity.ps1 +7 -1
  179. package/resources/codex-and-agents-customizations/.codex/hooks/check-python-test-purity.ps1 +7 -1
  180. package/resources/codex-and-agents-customizations/.codex/hooks/codex-epic-child-launch-attestation.ps1 +1 -1
  181. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +7 -1
  182. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +8 -2
  183. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +97 -44
  184. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +105 -0
  185. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-child-worktree-binding.ps1 +7 -1
  186. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +37 -5
  187. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +6 -0
  188. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-root-invocation.ps1 +7 -1
  189. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-wave-barrier.ps1 +7 -1
  190. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +54 -41
  191. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-evidence-locations.ps1 +7 -1
  192. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +35 -14
  193. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +31 -58
  194. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +63 -47
  195. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-targets.ps1 +433 -0
  196. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +24 -11
  197. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +8 -2
  198. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +13 -4
  199. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +8 -2
  200. package/resources/codex-and-agents-customizations/.codex/hooks/feature-folder-resolution.ps1 +373 -0
  201. package/resources/codex-and-agents-customizations/.codex/hooks/feature-review-coverage-thresholds.ps1 +85 -0
  202. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-heredoc.ps1 +119 -0
  203. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation-operands.ps1 +198 -0
  204. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +254 -255
  205. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload-powershell.ps1 +201 -0
  206. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload.ps1 +484 -0
  207. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +27 -110
  208. package/resources/codex-and-agents-customizations/.codex/hooks/hook-dependency-guard.ps1 +114 -0
  209. package/resources/codex-and-agents-customizations/.codex/hooks/hook-docs-only-diff.ps1 +493 -0
  210. package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +8 -2
  211. package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +7 -1
  212. package/resources/codex-and-agents-customizations/.codex/hooks/validate-feature-review-coverage.ps1 +24 -8
  213. package/resources/codex-and-agents-customizations/pack-manifests/core.json +9 -0
  214. package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +2 -2
  215. package/resources/customizations/.github/instructions/csharp-code-change.instructions.md +4 -4
  216. package/resources/customizations/.github/instructions/csharp-unit-test.instructions.md +2 -2
  217. package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +9 -1
  218. package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +4 -0
  219. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  220. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +1 -1
  221. package/resources/powershell/PoshQC/PoshQC.Coverage.psm1 +314 -0
  222. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +25 -28
  223. package/resources/powershell/PoshQC/PoshQC.psm1 +1 -0
  224. package/resources/powershell/PoshQC/README.md +8 -2
  225. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +2 -315
@@ -0,0 +1,201 @@
1
+ <#
2
+ .SYNOPSIS
3
+ PowerShell payload extraction and AST adapter for the payload-aware command iterator.
4
+ .DESCRIPTION
5
+ Issue #824. Reads the arguments of a pwsh or powershell invocation, extracts the
6
+ -Command / -CommandWithArgs payload or decodes the -EncodedCommand payload (base64
7
+ UTF-16LE), and turns a PowerShell payload into iterator records by parsing it with
8
+ [System.Management.Automation.Language.Parser]::ParseInput. Parsing never executes the
9
+ payload. A parse error yields one Unbalanced record; a decode failure is reported to the
10
+ caller, which marks the wrapper record Opaque with reason DecodeFailure.
11
+
12
+ Pure string logic only: no disk, process, network, clock, or environment access. It is
13
+ dot-sourced by hook-command-invocation.ps1, after hook-command-payload.ps1, whose record
14
+ builder and wrapper expansion it calls.
15
+ #>
16
+
17
+ # Host flags consumed before the command form (design item 4). The standalone flags take no
18
+ # value; the value flags take exactly one following token.
19
+ $script:CommandLinePowerShellStandaloneFlags = @(
20
+ '-noprofile', '-nop', '-nologo', '-nol', '-noninteractive', '-noni', '-noexit', '-noe',
21
+ '-login', '-l', '-mta', '-sta'
22
+ )
23
+ $script:CommandLinePowerShellValueFlags = @(
24
+ '-executionpolicy', '-ep', '-ex', '-workingdirectory', '-wd', '-windowstyle', '-w',
25
+ '-inputformat', '-if', '-outputformat', '-of', '-configurationname'
26
+ )
27
+
28
+ function ConvertFrom-CommandLineEncodedCommand {
29
+ <#
30
+ .SYNOPSIS
31
+ Decode an -EncodedCommand argument (base64 of UTF-16LE text).
32
+ .DESCRIPTION
33
+ Returns $null when the argument is not valid base64 or does not decode to whole
34
+ UTF-16 code units; the caller reports that as DecodeFailure.
35
+ .OUTPUTS
36
+ System.String or $null
37
+ #>
38
+ [CmdletBinding()]
39
+ [OutputType([string])]
40
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $EncodedText)
41
+
42
+ try {
43
+ $bytes = [System.Convert]::FromBase64String($EncodedText)
44
+ } catch [System.FormatException] {
45
+ return $null
46
+ }
47
+ if ($bytes.Length -eq 0 -or ($bytes.Length % 2) -ne 0) { return $null }
48
+ return [System.Text.Encoding]::Unicode.GetString($bytes)
49
+ }
50
+
51
+ function Get-CommandLinePowerShellPayload {
52
+ <#
53
+ .SYNOPSIS
54
+ Extract the payload from the arguments that follow a pwsh or powershell word.
55
+ .DESCRIPTION
56
+ Consumes the modeled host flags case-insensitively, then reads -c, -cwa,
57
+ -commandwithargs, any prefix of -command of length 4 or more, -e, -ec, or any prefix
58
+ of -encodedcommand of length 3 or more. Any other dash token, a non-dash token, or a
59
+ missing payload yields Opaque with reason NoPayload; an undecodable encoded payload
60
+ yields Opaque with reason DecodeFailure.
61
+ .OUTPUTS
62
+ System.Management.Automation.PSCustomObject with Payload, Opaque, OpaqueReason.
63
+ #>
64
+ [CmdletBinding()]
65
+ [OutputType([pscustomobject])]
66
+ param([Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Word)
67
+
68
+ $index = 0
69
+ while ($index -lt $Word.Count -and $Word[$index].StartsWith('-')) {
70
+ $flag = $Word[$index].ToLowerInvariant()
71
+ $remaining = [string[]]@($Word | Select-Object -Skip ($index + 1))
72
+ if ($script:CommandLinePowerShellStandaloneFlags -contains $flag) { $index++; continue }
73
+ if ($script:CommandLinePowerShellValueFlags -contains $flag) { $index += 2; continue }
74
+
75
+ $payload = $null
76
+ if ($flag -eq '-cwa' -or $flag -eq '-commandwithargs') {
77
+ if ($remaining.Count -gt 0) { $payload = $remaining[0] }
78
+ } elseif ($flag -eq '-c' -or ($flag.Length -ge 4 -and '-command'.StartsWith($flag))) {
79
+ if ($remaining.Count -gt 0) { $payload = $remaining -join ' ' }
80
+ } elseif ($flag -eq '-e' -or $flag -eq '-ec' -or ($flag.Length -ge 3 -and '-encodedcommand'.StartsWith($flag))) {
81
+ if ($remaining.Count -gt 0) {
82
+ $decoded = ConvertFrom-CommandLineEncodedCommand -EncodedText $remaining[0]
83
+ if ($null -eq $decoded) {
84
+ return [pscustomobject]@{ Payload = ''; Opaque = $true; OpaqueReason = 'DecodeFailure' }
85
+ }
86
+ $payload = $decoded
87
+ }
88
+ }
89
+ if (-not [string]::IsNullOrWhiteSpace($payload)) {
90
+ return [pscustomobject]@{ Payload = $payload; Opaque = $false; OpaqueReason = '' }
91
+ }
92
+ break
93
+ }
94
+ return [pscustomobject]@{ Payload = ''; Opaque = $true; OpaqueReason = 'NoPayload' }
95
+ }
96
+
97
+ function Get-CommandLinePowerShellNode {
98
+ <#
99
+ .SYNOPSIS
100
+ Return the command and non-command nodes of a parsed PowerShell payload in source order.
101
+ .DESCRIPTION
102
+ Every CommandAst anywhere in the tree, every top-level statement that is not a
103
+ pipeline made only of CommandAst elements, and every CommandExpressionAst pipeline
104
+ element. Sorted by start offset; a non-command node sorts before a command node at the
105
+ same offset, and duplicate non-command extents are dropped.
106
+ .OUTPUTS
107
+ System.Management.Automation.PSCustomObject[] with Node and IsCommand.
108
+ #>
109
+ [CmdletBinding()]
110
+ [OutputType([pscustomobject[]])]
111
+ param([Parameter(Mandatory)][System.Management.Automation.Language.ScriptBlockAst] $Ast)
112
+
113
+ $items = [System.Collections.Generic.List[pscustomobject]]::new()
114
+ $seen = [System.Collections.Generic.HashSet[string]]::new()
115
+ $isTopLevel = {
116
+ param($node)
117
+ $node -is [System.Management.Automation.Language.StatementAst] -and
118
+ $node.Parent -is [System.Management.Automation.Language.NamedBlockAst] -and
119
+ $node.Parent.Parent -is [System.Management.Automation.Language.ScriptBlockAst] -and
120
+ $null -eq $node.Parent.Parent.Parent
121
+ }
122
+ foreach ($statement in $Ast.FindAll($isTopLevel, $true)) {
123
+ $pureCommand = $statement -is [System.Management.Automation.Language.PipelineAst] -and
124
+ @($statement.PipelineElements | Where-Object { $_ -isnot [System.Management.Automation.Language.CommandAst] }).Count -eq 0
125
+ if (-not $pureCommand -and $seen.Add("$($statement.Extent.StartOffset):$($statement.Extent.EndOffset)")) {
126
+ $items.Add([pscustomobject]@{ Node = $statement; IsCommand = $false })
127
+ }
128
+ }
129
+ $isExpressionElement = { param($node) $node -is [System.Management.Automation.Language.CommandExpressionAst] -and $node.Parent -is [System.Management.Automation.Language.PipelineAst] }
130
+ foreach ($expression in $Ast.FindAll($isExpressionElement, $true)) {
131
+ if ($seen.Add("$($expression.Extent.StartOffset):$($expression.Extent.EndOffset)")) {
132
+ $items.Add([pscustomobject]@{ Node = $expression; IsCommand = $false })
133
+ }
134
+ }
135
+ foreach ($command in $Ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.CommandAst] }, $true)) {
136
+ $items.Add([pscustomobject]@{ Node = $command; IsCommand = $true })
137
+ }
138
+ return [pscustomobject[]]@($items | Sort-Object -Property @{ Expression = { $_.Node.Extent.StartOffset } }, @{ Expression = { $_.IsCommand } })
139
+ }
140
+
141
+ function Add-CommandLinePowerShellRecord {
142
+ <#
143
+ .SYNOPSIS
144
+ Parse a PowerShell payload and append one iterator record per command and non-command node.
145
+ .DESCRIPTION
146
+ A CommandAst record's tokens are the element values: the Value of a string constant,
147
+ the text of a parameter, and the extent text of anything else. The record is Literal
148
+ only when every element is a string constant or a parameter without an argument and
149
+ the command carries no redirection. A CommandAst whose name is itself a wrapper is
150
+ expanded in turn. A parse error yields a single Unbalanced record carrying the payload.
151
+ #>
152
+ [CmdletBinding()]
153
+ param(
154
+ [Parameter(Mandatory)][hashtable] $Context,
155
+ [Parameter(Mandatory)][AllowEmptyString()][string] $Text,
156
+ [Parameter(Mandatory)][int] $Depth,
157
+ [Parameter(Mandatory)][AllowEmptyString()][string] $Wrapper,
158
+ [Parameter(Mandatory)][int] $RootIndex,
159
+ [Parameter(Mandatory)][AllowEmptyString()][string] $RootRawText,
160
+ [Parameter(Mandatory)][bool] $ArgumentInjected
161
+ )
162
+
163
+ $common = @{
164
+ Context = $Context; Depth = $Depth; Origin = 'WrapperPayload'; Wrapper = $Wrapper; Dialect = 'PowerShell'
165
+ RootIndex = $RootIndex; RootRawText = $RootRawText; Delimiter = ''; ArgumentInjected = $ArgumentInjected
166
+ }
167
+ $parseTokens = $null
168
+ $parseErrors = $null
169
+ $ast = [System.Management.Automation.Language.Parser]::ParseInput($Text, [ref]$parseTokens, [ref]$parseErrors)
170
+ if (@($parseErrors).Count -gt 0) {
171
+ $null = Add-CommandLineInvocationRecord @common -RawText $Text -Tokens @() -CommandWord '' -Literal $false -Unbalanced $true
172
+ return
173
+ }
174
+
175
+ foreach ($item in @(Get-CommandLinePowerShellNode -Ast $ast)) {
176
+ if (-not $item.IsCommand) {
177
+ $null = Add-CommandLineInvocationRecord @common -RawText $item.Node.Extent.Text -Tokens @() -CommandWord '' -Literal $false -Unbalanced $false
178
+ continue
179
+ }
180
+ $command = $item.Node
181
+ $tokens = [System.Collections.Generic.List[string]]::new()
182
+ $literal = $command.Redirections.Count -eq 0
183
+ foreach ($element in $command.CommandElements) {
184
+ if ($element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
185
+ $tokens.Add($element.Value)
186
+ } elseif ($element -is [System.Management.Automation.Language.CommandParameterAst]) {
187
+ $tokens.Add($element.Extent.Text)
188
+ if ($null -ne $element.Argument) { $literal = $false }
189
+ } else {
190
+ $tokens.Add($element.Extent.Text)
191
+ $literal = $false
192
+ }
193
+ }
194
+ $nameIsConstant = $command.CommandElements[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]
195
+ $commandWord = if ($nameIsConstant) { ConvertTo-CommandLineLeafWord -Word $tokens[0] } else { $tokens[0] }
196
+ $record = Add-CommandLineInvocationRecord @common -RawText $command.Extent.Text -Tokens $tokens.ToArray() -CommandWord $commandWord -Literal $literal -Unbalanced $false
197
+ if ($nameIsConstant) {
198
+ Add-CommandLineWrapperExpansion -Context $Context -Record $record -Word $tokens.ToArray()
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,484 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Payload-aware command-line iterator shared by the Claude and Codex enforcement hooks.
4
+ .DESCRIPTION
5
+ Issue #824. Read-CommandLineInvocationSegment returns one record per command visible in a
6
+ Bash command line, in pre-order source order: each top-level scanner segment, each command
7
+ inside a wrapper payload (sh, bash, zsh, dash, ksh, pwsh, powershell, eval), each command
8
+ inside a double-quoted command substitution, and the command xargs injects arguments into.
9
+ PowerShell payloads are parsed by hook-command-payload-powershell.ps1. This file also holds
10
+ the whole-token presence predicate and the inert-payload proof.
11
+
12
+ Pure string logic only: no disk, process, network, clock, or environment access. It is
13
+ dot-sourced by hook-command-invocation.ps1, after hook-command-scanner.ps1.
14
+ #>
15
+
16
+ # Wrapper words whose argument is itself a command line (design item 4).
17
+ $script:CommandLinePosixShellNames = @('sh', 'bash', 'zsh', 'dash', 'ksh')
18
+ $script:CommandLinePowerShellHostNames = @('pwsh', 'powershell')
19
+
20
+ # The fixed sink allowlist of the inert proof (spec design item 8). Not extended here.
21
+ $script:CommandLineInertSinkNames = @(
22
+ 'Write-Output', 'Write-Host', 'Write-Verbose', 'Write-Information',
23
+ 'echo', 'printf', 'Select-String', 'grep', 'rg'
24
+ )
25
+
26
+ # xargs options that consume the following token when no value is attached.
27
+ $script:CommandLineXargsValueOptions = @('-n', '-I', '-L', '-d', '-P', '-s', '-E', '-a')
28
+
29
+ function ConvertTo-CommandLineLeafWord {
30
+ <#
31
+ .SYNOPSIS
32
+ Return the text after the last '/' or '\', with a trailing '.exe' removed
33
+ case-insensitively. Other case is preserved; callers compare case-insensitively.
34
+ #>
35
+ [CmdletBinding()]
36
+ [OutputType([string])]
37
+ param([Parameter(Mandatory)][AllowEmptyString()][AllowNull()][string] $Word)
38
+
39
+ if ([string]::IsNullOrEmpty($Word)) { return '' }
40
+ $leaf = $Word.Substring($Word.LastIndexOfAny([char[]]@('/', '\')) + 1)
41
+ if ($leaf.EndsWith('.exe', [System.StringComparison]::OrdinalIgnoreCase)) {
42
+ $leaf = $leaf.Substring(0, $leaf.Length - 4)
43
+ }
44
+ return $leaf
45
+ }
46
+
47
+ function Test-CommandLineWordPresent {
48
+ <#
49
+ .SYNOPSIS
50
+ Report whether a word occurs in a text as a whole token.
51
+ .DESCRIPTION
52
+ Matches (?<![\w-])<word>(?![\w-]) with IgnoreCase and CultureInvariant, so 'gh' is not
53
+ present in 'through' and 'new' is not present in 'New-Object'. This is the only
54
+ presence implementation the classifier uses.
55
+ .OUTPUTS
56
+ System.Boolean
57
+ #>
58
+ [CmdletBinding()]
59
+ [OutputType([bool])]
60
+ param(
61
+ [Parameter(Mandatory)][AllowEmptyString()][AllowNull()][string] $RawText,
62
+ [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $Word
63
+ )
64
+
65
+ if ([string]::IsNullOrEmpty($RawText)) { return $false }
66
+ $pattern = '(?<![\w-])' + [regex]::Escape($Word) + '(?![\w-])'
67
+ $options = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::CultureInvariant
68
+ return [regex]::IsMatch($RawText, $pattern, $options)
69
+ }
70
+
71
+ function ConvertTo-CommandLineNormalizedText {
72
+ <#
73
+ .SYNOPSIS
74
+ Pre-normalize POSIX command text before it is segmented.
75
+ .DESCRIPTION
76
+ Replaces each backslash + optional CR + LF with one space (design item 1), and quotes an
77
+ unquoted standalone '{}' word as '{}': the shell reads that word as a literal (the
78
+ xargs -I and find -exec placeholder), but the scanner treats every brace as a group
79
+ delimiter, which would detach an xargs payload from its xargs word.
80
+ #>
81
+ [CmdletBinding()]
82
+ [OutputType([string])]
83
+ param([Parameter(Mandatory)][AllowEmptyString()][AllowNull()][string] $CommandText)
84
+
85
+ if ([string]::IsNullOrEmpty($CommandText)) { return '' }
86
+ $text = [regex]::Replace($CommandText, '\\\r?\n', ' ')
87
+ $builder = [System.Text.StringBuilder]::new()
88
+ $quote = [char]0
89
+ for ($index = 0; $index -lt $text.Length; $index++) {
90
+ $character = $text[$index]
91
+ $hasNext = $index + 1 -lt $text.Length
92
+ if ($character -eq '\' -and $hasNext -and $quote -ne "'") {
93
+ [void]$builder.Append($character).Append($text[$index + 1])
94
+ $index++
95
+ continue
96
+ }
97
+ if ($quote -ne [char]0) {
98
+ if ($character -eq $quote) { $quote = [char]0 }
99
+ [void]$builder.Append($character)
100
+ continue
101
+ }
102
+ if ($character -eq '"' -or $character -eq "'") { $quote = $character }
103
+ $standalone = $character -eq '{' -and $hasNext -and $text[$index + 1] -eq '}' -and
104
+ ($index -eq 0 -or [char]::IsWhiteSpace($text[$index - 1])) -and
105
+ ($index + 2 -ge $text.Length -or $text[$index + 2] -match '[\s;|&)\\]')
106
+ if ($standalone) {
107
+ [void]$builder.Append("'{}'")
108
+ $index++
109
+ continue
110
+ }
111
+ [void]$builder.Append($character)
112
+ }
113
+ return $builder.ToString()
114
+ }
115
+
116
+ function Split-CommandLinePosixWord {
117
+ <#
118
+ .SYNOPSIS
119
+ Split POSIX text into shell words, removing quotes and backslash escapes.
120
+ .DESCRIPTION
121
+ Single quotes are literal; inside double quotes a backslash escapes '"', '\', '$', and
122
+ the backtick; outside quotes it escapes any character. Used to read a wrapper's
123
+ arguments, so bash -c "\"gh\" issue create" yields the payload '"gh" issue create'.
124
+ #>
125
+ [CmdletBinding()]
126
+ [OutputType([string[]])]
127
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Text)
128
+
129
+ $words = [System.Collections.Generic.List[string]]::new()
130
+ $current = [System.Text.StringBuilder]::new()
131
+ $hasWord = $false
132
+ $quote = [char]0
133
+ for ($index = 0; $index -lt $Text.Length; $index++) {
134
+ $character = $Text[$index]
135
+ $next = if ($index + 1 -lt $Text.Length) { $Text[$index + 1] } else { [char]0 }
136
+ if ($quote -eq "'") {
137
+ if ($character -eq "'") { $quote = [char]0 } else { [void]$current.Append($character) }
138
+ continue
139
+ }
140
+ if ($quote -eq '"') {
141
+ if ($character -eq '\' -and $next -ne [char]0 -and '"\$`'.IndexOf($next) -ge 0) { [void]$current.Append($next); $index++ }
142
+ elseif ($character -eq '"') { $quote = [char]0 }
143
+ else { [void]$current.Append($character) }
144
+ continue
145
+ }
146
+ if ($character -eq '\' -and $next -ne [char]0) {
147
+ [void]$current.Append($next)
148
+ $hasWord = $true
149
+ $index++
150
+ continue
151
+ }
152
+ if ($character -eq '"' -or $character -eq "'") {
153
+ $quote = $character
154
+ $hasWord = $true
155
+ continue
156
+ }
157
+ if ([char]::IsWhiteSpace($character)) {
158
+ if ($hasWord) { $words.Add($current.ToString()); [void]$current.Clear(); $hasWord = $false }
159
+ continue
160
+ }
161
+ [void]$current.Append($character)
162
+ $hasWord = $true
163
+ }
164
+ if ($hasWord) { $words.Add($current.ToString()) }
165
+ return [string[]]$words.ToArray()
166
+ }
167
+
168
+ function Skip-CommandLineTransparentWrapper {
169
+ <#
170
+ .SYNOPSIS
171
+ Return Index, the position of the command word after VAR=value prefixes and transparent
172
+ wrappers, and SkippedWrapper.
173
+ .DESCRIPTION
174
+ Skips env with its dash and VAR=value tokens, timeout with its dash tokens and one
175
+ duration token, time -p, command -p|-v|-V, and nohup (design item 4). Except names a
176
+ word that is never skipped, so a query for that word can match it.
177
+ #>
178
+ [CmdletBinding()]
179
+ [OutputType([pscustomobject])]
180
+ param(
181
+ [Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Token,
182
+ [AllowEmptyString()][string] $Except = ''
183
+ )
184
+
185
+ $index = 0
186
+ $skipped = $false
187
+ while ($index -lt $Token.Count) {
188
+ if ($Token[$index] -match '^[A-Za-z_][A-Za-z0-9_]*=') { $index++; continue }
189
+ $leaf = (ConvertTo-CommandLineLeafWord -Word $Token[$index]).ToLowerInvariant()
190
+ if ($leaf -eq $Except.ToLowerInvariant() -or $script:CommandLineTransparentWrapperNames -notcontains $leaf) { break }
191
+ $skipped = $true
192
+ $index++
193
+ switch ($leaf) {
194
+ 'env' {
195
+ while ($index -lt $Token.Count -and ($Token[$index].StartsWith('-') -or $Token[$index] -match '^[A-Za-z_][A-Za-z0-9_]*=')) { $index++ }
196
+ }
197
+ 'timeout' {
198
+ while ($index -lt $Token.Count -and $Token[$index].StartsWith('-')) { $index++ }
199
+ if ($index -lt $Token.Count) { $index++ }
200
+ }
201
+ 'time' { if ($index -lt $Token.Count -and $Token[$index] -eq '-p') { $index++ } }
202
+ 'command' { while ($index -lt $Token.Count -and @('-p', '-v', '-V') -ccontains $Token[$index]) { $index++ } }
203
+ }
204
+ }
205
+ return [pscustomobject]@{ Index = $index; SkippedWrapper = $skipped }
206
+ }
207
+
208
+ function Get-CommandLineWrapperPayload {
209
+ <#
210
+ .SYNOPSIS
211
+ Identify a payload wrapper in a word list and extract its payload.
212
+ .DESCRIPTION
213
+ Returns Wrapper ('' when the words are not led by a payload wrapper), Kind ('Posix',
214
+ 'PowerShell', or 'Words'), Payload, Words (the xargs-injected command), Opaque, and
215
+ OpaqueReason. A wrapper with no extractable payload is Opaque with reason NoPayload.
216
+ #>
217
+ [CmdletBinding()]
218
+ [OutputType([pscustomobject])]
219
+ param([Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Word)
220
+
221
+ $result = [pscustomobject]@{ Wrapper = ''; Kind = ''; Payload = ''; Words = [string[]]@(); Opaque = $false; OpaqueReason = '' }
222
+ $start = (Skip-CommandLineTransparentWrapper -Token $Word).Index
223
+ if ($start -ge $Word.Count) { return $result }
224
+ $wrapper = (ConvertTo-CommandLineLeafWord -Word $Word[$start]).ToLowerInvariant()
225
+ $rest = [string[]]@($Word | Select-Object -Skip ($start + 1))
226
+
227
+ if ($script:CommandLinePosixShellNames -contains $wrapper) {
228
+ $result.Wrapper = $wrapper
229
+ for ($index = 0; $index -lt $rest.Count -and $rest[$index].StartsWith('-'); $index++) {
230
+ if ($rest[$index] -cmatch '^-[A-Za-z]*c[A-Za-z]*$' -and $index + 1 -lt $rest.Count) {
231
+ $result.Kind = 'Posix'; $result.Payload = $rest[$index + 1]; return $result
232
+ }
233
+ }
234
+ } elseif ($script:CommandLinePowerShellHostNames -contains $wrapper) {
235
+ # An Opaque result carries NoPayload or DecodeFailure from the PowerShell adapter.
236
+ $powerShell = Get-CommandLinePowerShellPayload -Word $rest
237
+ $result.Wrapper = $wrapper; $result.Kind = 'PowerShell'; $result.Payload = $powerShell.Payload
238
+ $result.Opaque = $powerShell.Opaque; $result.OpaqueReason = $powerShell.OpaqueReason
239
+ return $result
240
+ } elseif ($wrapper -eq 'eval') {
241
+ $result.Wrapper = $wrapper
242
+ if ($rest.Count -gt 0) { $result.Kind = 'Posix'; $result.Payload = $rest -join ' '; return $result }
243
+ } elseif ($wrapper -eq 'xargs') {
244
+ $result.Wrapper = $wrapper
245
+ $index = 0
246
+ while ($index -lt $rest.Count -and $rest[$index].StartsWith('-')) {
247
+ $index += if ($script:CommandLineXargsValueOptions -ccontains $rest[$index]) { 2 } else { 1 }
248
+ }
249
+ if ($index -lt $rest.Count) {
250
+ $result.Kind = 'Words'; $result.Words = [string[]]@($rest | Select-Object -Skip $index); return $result
251
+ }
252
+ } else {
253
+ return $result
254
+ }
255
+
256
+ $result.Opaque = $true
257
+ $result.OpaqueReason = 'NoPayload'
258
+ return $result
259
+ }
260
+
261
+ function Get-CommandLineSubstitutionBody {
262
+ <#
263
+ .SYNOPSIS
264
+ Return the content of each double-quoted span that contains '$(' or a backtick.
265
+ #>
266
+ [CmdletBinding()]
267
+ [OutputType([string[]])]
268
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Text)
269
+
270
+ $bodies = [System.Collections.Generic.List[string]]::new()
271
+ $span = [System.Text.StringBuilder]::new()
272
+ $quote = [char]0
273
+ for ($index = 0; $index -lt $Text.Length; $index++) {
274
+ $character = $Text[$index]
275
+ if ($quote -eq "'") {
276
+ if ($character -eq "'") { $quote = [char]0 }
277
+ continue
278
+ }
279
+ if ($character -eq '\' -and $index + 1 -lt $Text.Length) {
280
+ if ($quote -eq '"') { [void]$span.Append($character).Append($Text[$index + 1]) }
281
+ $index++
282
+ continue
283
+ }
284
+ if ($quote -eq '"') {
285
+ if ($character -ne '"') { [void]$span.Append($character); continue }
286
+ $quote = [char]0
287
+ $body = $span.ToString()
288
+ if ($body.Contains('$(') -or $body.Contains('`')) { $bodies.Add($body) }
289
+ continue
290
+ }
291
+ if ($character -eq "'") { $quote = $character }
292
+ if ($character -eq '"') { $quote = $character; [void]$span.Clear() }
293
+ }
294
+ return [string[]]$bodies.ToArray()
295
+ }
296
+
297
+ function Add-CommandLineInvocationRecord {
298
+ <#
299
+ .SYNOPSIS
300
+ Build one iterator record, append it to the context's record list, and return it.
301
+ PresenceText follows design item DC-9: TopLevel uses RawText, WrapperPayload uses
302
+ RootRawText, Substitution uses SubstitutionPresence, ArgumentInjector uses the whole
303
+ normalized command text.
304
+ #>
305
+ [CmdletBinding()]
306
+ [OutputType([pscustomobject])]
307
+ param(
308
+ [Parameter(Mandatory)][hashtable] $Context, [Parameter(Mandatory)][int] $Depth,
309
+ [Parameter(Mandatory)][string] $Origin, [Parameter(Mandatory)][AllowEmptyString()][string] $Wrapper,
310
+ [Parameter(Mandatory)][string] $Dialect, [Parameter(Mandatory)][int] $RootIndex,
311
+ [Parameter(Mandatory)][AllowEmptyString()][string] $RootRawText, [Parameter(Mandatory)][AllowEmptyString()][string] $RawText,
312
+ [Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Tokens,
313
+ [Parameter(Mandatory)][AllowEmptyString()][string] $CommandWord, [Parameter(Mandatory)][bool] $Literal,
314
+ [Parameter(Mandatory)][bool] $Unbalanced, [Parameter(Mandatory)][AllowEmptyString()][string] $Delimiter,
315
+ [Parameter(Mandatory)][bool] $ArgumentInjected, [AllowEmptyString()][string] $SubstitutionPresence = ''
316
+ )
317
+
318
+ $index = $Context.Records.Count
319
+ $rootIndexValue = if ($RootIndex -lt 0) { $index } else { $RootIndex }
320
+ $rootRaw = if ($RootIndex -lt 0) { $RawText } else { $RootRawText }
321
+ $presence = switch ($Origin) {
322
+ 'TopLevel' { $RawText }
323
+ 'WrapperPayload' { $rootRaw }
324
+ 'Substitution' { $SubstitutionPresence }
325
+ default { $Context.NormalizedText }
326
+ }
327
+ $record = [pscustomobject]@{
328
+ Index = $index; RootIndex = $rootIndexValue; Depth = $Depth; Origin = $Origin; Wrapper = $Wrapper
329
+ Dialect = $Dialect; RawText = $RawText; RootRawText = $rootRaw; Tokens = [string[]]$Tokens
330
+ CommandWord = $CommandWord; Literal = $Literal; Unbalanced = $Unbalanced; Opaque = $false
331
+ OpaqueReason = ''; Delimiter = $Delimiter; PresenceText = $presence; ArgumentInjected = $ArgumentInjected
332
+ }
333
+ $Context.Records.Add($record)
334
+ return $record
335
+ }
336
+
337
+ function Add-CommandLineWrapperExpansion {
338
+ <#
339
+ .SYNOPSIS
340
+ Expand the wrapper payload of one record into child records, or mark the record Opaque.
341
+ #>
342
+ [CmdletBinding()]
343
+ param(
344
+ [Parameter(Mandatory)][hashtable] $Context,
345
+ [Parameter(Mandatory)][pscustomobject] $Record,
346
+ [Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Word
347
+ )
348
+
349
+ $payload = Get-CommandLineWrapperPayload -Word $Word
350
+ if ([string]::IsNullOrEmpty($payload.Wrapper)) { return }
351
+ if ($payload.Opaque -or $Record.Depth + 1 -gt $Context.MaxDepth) {
352
+ $Record.Opaque = $true
353
+ $Record.OpaqueReason = if ($payload.Opaque) { $payload.OpaqueReason } else { 'DepthLimit' }
354
+ return
355
+ }
356
+
357
+ $child = @{
358
+ Context = $Context; Depth = $Record.Depth + 1; Wrapper = $payload.Wrapper; RootIndex = $Record.RootIndex
359
+ RootRawText = $Record.RootRawText; ArgumentInjected = [bool]$Record.ArgumentInjected
360
+ }
361
+ switch ($payload.Kind) {
362
+ 'Posix' { Add-CommandLinePosixRecord @child -Origin 'WrapperPayload' -Text $payload.Payload }
363
+ 'PowerShell' { Add-CommandLinePowerShellRecord @child -Text $payload.Payload }
364
+ 'Words' {
365
+ $words = [string[]]$payload.Words
366
+ $child.ArgumentInjected = $true
367
+ $injected = Add-CommandLineInvocationRecord @child -Origin 'ArgumentInjector' -Dialect 'Posix' -RawText ($words -join ' ') `
368
+ -Tokens $words -CommandWord (ConvertTo-CommandLineLeafWord -Word $words[0]) -Literal (Test-CommandLinePosixTokenLiteral -Token $words) `
369
+ -Unbalanced $false -Delimiter ''
370
+ Add-CommandLineWrapperExpansion -Context $Context -Record $injected -Word $words
371
+ }
372
+ }
373
+ }
374
+
375
+ function Test-CommandLinePosixTokenLiteral {
376
+ <#
377
+ .SYNOPSIS
378
+ Report whether no token carries a '$' or a backtick.
379
+ #>
380
+ [CmdletBinding()]
381
+ [OutputType([bool])]
382
+ param([Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Token)
383
+
384
+ foreach ($current in $Token) {
385
+ if ($current.Contains('$') -or $current.Contains('`')) { return $false }
386
+ }
387
+ return $true
388
+ }
389
+
390
+ function Add-CommandLinePosixRecord {
391
+ <#
392
+ .SYNOPSIS
393
+ Segment POSIX text and append one record per segment, expanding wrappers and
394
+ substitution bodies in pre-order.
395
+ #>
396
+ [CmdletBinding()]
397
+ param(
398
+ [Parameter(Mandatory)][hashtable] $Context, [Parameter(Mandatory)][AllowEmptyString()][string] $Text,
399
+ [Parameter(Mandatory)][int] $Depth, [Parameter(Mandatory)][string] $Origin,
400
+ [Parameter(Mandatory)][AllowEmptyString()][string] $Wrapper, [Parameter(Mandatory)][int] $RootIndex,
401
+ [Parameter(Mandatory)][AllowEmptyString()][string] $RootRawText, [Parameter(Mandatory)][bool] $ArgumentInjected
402
+ )
403
+
404
+ $segments = @(Read-CommandLineSegment -CommandText (ConvertTo-CommandLineNormalizedText -CommandText $Text))
405
+ $presence = (@($segments | ForEach-Object { $_.TokenText }) -join ' ')
406
+ foreach ($segment in $segments) {
407
+ $tokens = [string[]]@($segment.Tokens)
408
+ $record = Add-CommandLineInvocationRecord -Context $Context -Depth $Depth -Origin $Origin -Wrapper $Wrapper -Dialect 'Posix' `
409
+ -RootIndex $RootIndex -RootRawText $RootRawText -RawText $segment.RawText -Tokens $tokens `
410
+ -CommandWord (ConvertTo-CommandLineLeafWord -Word $segment.CommandWord) -Literal (Test-CommandLinePosixTokenLiteral -Token $tokens) `
411
+ -Unbalanced ([bool]$segment.Unbalanced) -Delimiter $segment.Delimiter -ArgumentInjected $ArgumentInjected -SubstitutionPresence $presence
412
+ Add-CommandLineWrapperExpansion -Context $Context -Record $record -Word (Split-CommandLinePosixWord -Text $segment.TokenText)
413
+ if (-not $segment.HasLiveSubstitution) { continue }
414
+ foreach ($body in @(Get-CommandLineSubstitutionBody -Text $segment.TokenText)) {
415
+ if ($Depth + 1 -gt $Context.MaxDepth) {
416
+ $record.Opaque = $true
417
+ $record.OpaqueReason = 'DepthLimit'
418
+ break
419
+ }
420
+ Add-CommandLinePosixRecord -Context $Context -Text $body -Depth ($Depth + 1) -Origin 'Substitution' -Wrapper '' `
421
+ -RootIndex $record.RootIndex -RootRawText $record.RootRawText -ArgumentInjected ([bool]$record.ArgumentInjected)
422
+ }
423
+ }
424
+ }
425
+
426
+ function Read-CommandLineInvocationSegment {
427
+ <#
428
+ .SYNOPSIS
429
+ Return one record per command visible in a command line, including wrapper payloads,
430
+ substitution bodies, and xargs-injected commands, in pre-order source order.
431
+ .PARAMETER CommandText
432
+ The raw Bash command text.
433
+ .PARAMETER MaxDepth
434
+ The deepest payload level expanded. A wrapper whose payload would exceed it is Opaque
435
+ with reason DepthLimit and yields no payload records.
436
+ .OUTPUTS
437
+ System.Management.Automation.PSCustomObject[] - each record carries Index, RootIndex,
438
+ Depth, Origin ('TopLevel', 'WrapperPayload', 'Substitution', 'ArgumentInjector'),
439
+ Wrapper, Dialect ('Posix', 'PowerShell'), RawText, RootRawText, Tokens, CommandWord
440
+ (leaf-normalized), Literal, Unbalanced, Opaque, OpaqueReason ('', 'NoPayload',
441
+ 'DecodeFailure', 'DepthLimit'), Delimiter, PresenceText, and ArgumentInjected.
442
+ #>
443
+ [CmdletBinding()]
444
+ [OutputType([pscustomobject[]])]
445
+ param(
446
+ [Parameter(Mandatory)][AllowEmptyString()][AllowNull()][string] $CommandText,
447
+ [ValidateRange(0, 64)][int] $MaxDepth = 4
448
+ )
449
+
450
+ $records = [System.Collections.Generic.List[pscustomobject]]::new()
451
+ if ([string]::IsNullOrWhiteSpace($CommandText)) { return $records.ToArray() }
452
+ $normalized = ConvertTo-CommandLineNormalizedText -CommandText $CommandText
453
+ $context = @{ Records = $records; MaxDepth = $MaxDepth; NormalizedText = $normalized }
454
+ Add-CommandLinePosixRecord -Context $context -Text $normalized -Depth 0 -Origin 'TopLevel' -Wrapper '' -RootIndex -1 -RootRawText '' -ArgumentInjected $false
455
+ return $records.ToArray()
456
+ }
457
+
458
+ function Test-CommandLinePayloadInert {
459
+ <#
460
+ .SYNOPSIS
461
+ Report whether the payload of one root is proven inert (design item 8, D3).
462
+ .DESCRIPTION
463
+ True only when every record with that RootIndex and Depth >= 1 is Literal, neither
464
+ Unbalanced nor Opaque, led by a command in the fixed sink allowlist, and carries no
465
+ token containing '<' or '>'. A root with no payload records is not proven inert.
466
+ .OUTPUTS
467
+ System.Boolean
468
+ #>
469
+ [CmdletBinding()]
470
+ [OutputType([bool])]
471
+ param(
472
+ [Parameter(Mandatory)][AllowEmptyCollection()][object[]] $Record,
473
+ [Parameter(Mandatory)][int] $RootIndex
474
+ )
475
+
476
+ $payload = @($Record | Where-Object { $_.RootIndex -eq $RootIndex -and $_.Depth -ge 1 })
477
+ if ($payload.Count -eq 0) { return $false }
478
+ foreach ($current in $payload) {
479
+ if (-not $current.Literal -or $current.Unbalanced -or $current.Opaque) { return $false }
480
+ if ($script:CommandLineInertSinkNames -notcontains (ConvertTo-CommandLineLeafWord -Word $current.CommandWord)) { return $false }
481
+ if (@($current.Tokens | Where-Object { $_.Contains('<') -or $_.Contains('>') }).Count -gt 0) { return $false }
482
+ }
483
+ return $true
484
+ }