@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.
- package/out/mcp-server.js +1405 -684
- package/package.json +6 -4
- package/resources/claude-customizations/.claude/agents/atomic-executor.md +11 -0
- package/resources/claude-customizations/.claude/agents/atomic-planner.md +10 -0
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +12 -4
- package/resources/claude-customizations/.claude/agents/epic-planner.md +5 -4
- package/resources/claude-customizations/.claude/agents/feature-review.md +16 -5
- package/resources/claude-customizations/.claude/agents/orchestrator.md +21 -7
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +37 -13
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +9 -6
- package/resources/claude-customizations/.claude/agents/pr-author.md +15 -2
- package/resources/claude-customizations/.claude/agents/prd-feature.md +1 -1
- package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +7 -1
- package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +7 -1
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +7 -1
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +111 -46
- package/resources/claude-customizations/.claude/hooks/enforce-completion-helpers.ps1 +105 -0
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +8 -1
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +7 -1
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +116 -59
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +55 -23
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +74 -75
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +127 -35
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +61 -57
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +7 -1
- package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +129 -39
- package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +7 -1
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +9 -3
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +39 -51
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +31 -58
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +63 -47
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-targets.ps1 +433 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +17 -12
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +9 -3
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +65 -50
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +49 -42
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +54 -72
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +8 -2
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +279 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +129 -75
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.artifact-root.ps1 +233 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +224 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +69 -24
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +29 -18
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +10 -4
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +14 -5
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +8 -2
- package/resources/claude-customizations/.claude/hooks/feature-folder-resolution.ps1 +373 -0
- package/resources/claude-customizations/.claude/hooks/feature-review-coverage-thresholds.ps1 +85 -0
- package/resources/claude-customizations/.claude/hooks/hook-command-heredoc.ps1 +119 -0
- package/resources/claude-customizations/.claude/hooks/hook-command-invocation-operands.ps1 +198 -0
- package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +254 -255
- package/resources/claude-customizations/.claude/hooks/hook-command-payload-powershell.ps1 +201 -0
- package/resources/claude-customizations/.claude/hooks/hook-command-payload.ps1 +484 -0
- package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +27 -110
- package/resources/claude-customizations/.claude/hooks/hook-dependency-guard.ps1 +114 -0
- package/resources/claude-customizations/.claude/hooks/hook-docs-only-diff.ps1 +493 -0
- package/resources/claude-customizations/.claude/hooks/subagentstop-envelope.ps1 +192 -0
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +9 -3
- package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +40 -42
- package/resources/claude-customizations/.claude/hooks/validate-executor-output.ps1 +66 -38
- package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-metrics.ps1 +180 -0
- package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-rules.ps1 +195 -0
- package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage.ps1 +141 -395
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-resolution.ps1 +311 -0
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-routing.ps1 +170 -0
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +115 -190
- package/resources/claude-customizations/.claude/hooks/validate-planner-output.ps1 +64 -32
- package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +39 -44
- package/resources/claude-customizations/.claude/hooks/validate-prd-feature-output.ps1 +43 -14
- package/resources/claude-customizations/.claude/hooks/validate-required-artifact-output.ps1 +66 -36
- package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +31 -34
- package/resources/claude-customizations/.claude/lib/bash/mutate-parallel-run.sh +191 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-cohorts.sh +13 -3
- package/resources/claude-customizations/.claude/lib/bash/parallel-lane-assertion.sh +4 -2
- package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-cli.sh +152 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-ops.sh +193 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-mutation.sh +322 -0
- package/resources/claude-customizations/.claude/lib/bash/remove-parallel-item.sh +146 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +23 -33
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +86 -6
- package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +91 -19
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -13
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateEpicWaveBarrier.psm1 +346 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +396 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +22 -15
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +337 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +9 -1
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +79 -2
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +3 -3
- package/resources/claude-customizations/.claude/rules/architecture-boundaries.md +5 -5
- package/resources/claude-customizations/.claude/rules/csharp.md +2 -2
- package/resources/claude-customizations/.claude/rules/general-code-change.md +1 -1
- package/resources/claude-customizations/.claude/rules/general-unit-test.md +3 -2
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +153 -4
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +50 -5
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +47 -8
- package/resources/claude-customizations/.claude/rules/quality-tiers.md +11 -7
- package/resources/claude-customizations/.claude/rules/shell.md +10 -8
- package/resources/claude-customizations/.claude/rules/typescript.md +1 -1
- package/resources/claude-customizations/.claude/settings.json +3 -4
- package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +9 -1
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +7 -1
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +11 -3
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +11 -2
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +1 -1
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +171 -5
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +1 -1
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +6 -43
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +6 -15
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +45 -11
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +10 -0
- package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +7 -4
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +42 -16
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +52 -14
- package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +22 -9
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +97 -19
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +63 -7
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +35 -12
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +7 -3
- package/resources/claude-customizations/pack-manifests/core.json +25 -0
- package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +9 -1
- package/resources/codex-and-agents-customizations/.agents/skills/architecture-boundaries/SKILL.md +5 -5
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-executor/SKILL.md +11 -0
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +4 -0
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +10 -0
- package/resources/codex-and-agents-customizations/.agents/skills/csharp/SKILL.md +2 -2
- package/resources/codex-and-agents-customizations/.agents/skills/csharp-qa-gate/SKILL.md +2 -2
- package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +13 -3
- package/resources/codex-and-agents-customizations/.agents/skills/epic-plan/SKILL.md +17 -1
- package/resources/codex-and-agents-customizations/.agents/skills/epic-run/SKILL.md +2 -1
- package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +30 -6
- package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +16 -6
- package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +3 -2
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +43 -11
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -2
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +19 -4
- package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +11 -7
- package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +2 -0
- package/resources/codex-and-agents-customizations/.agents/skills/translate-claude-to-codex/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +2 -2
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +11 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +11 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +11 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +11 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +11 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +11 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +10 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +10 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +10 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +10 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +10 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +10 -0
- package/resources/codex-and-agents-customizations/.codex/agents/epic-orchestrator.toml +2 -1
- package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +6 -6
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/codex-web-setup.sh +318 -273
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/check-powershell-test-purity.ps1 +7 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/check-python-test-purity.ps1 +7 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/codex-epic-child-launch-attestation.ps1 +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +7 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +8 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +97 -44
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +105 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-child-worktree-binding.ps1 +7 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +37 -5
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +6 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-root-invocation.ps1 +7 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-wave-barrier.ps1 +7 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +54 -41
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-evidence-locations.ps1 +7 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +35 -14
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +31 -58
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +63 -47
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-targets.ps1 +433 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +24 -11
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +8 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +13 -4
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +8 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/feature-folder-resolution.ps1 +373 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/feature-review-coverage-thresholds.ps1 +85 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-heredoc.ps1 +119 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation-operands.ps1 +198 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +254 -255
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload-powershell.ps1 +201 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload.ps1 +484 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +27 -110
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-dependency-guard.ps1 +114 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-docs-only-diff.ps1 +493 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +8 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +7 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-feature-review-coverage.ps1 +24 -8
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +9 -0
- package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +2 -2
- package/resources/customizations/.github/instructions/csharp-code-change.instructions.md +4 -4
- package/resources/customizations/.github/instructions/csharp-unit-test.instructions.md +2 -2
- package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +9 -1
- package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +4 -0
- package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +1 -1
- package/resources/powershell/PoshQC/PoshQC.Coverage.psm1 +314 -0
- package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +25 -28
- package/resources/powershell/PoshQC/PoshQC.psm1 +1 -0
- package/resources/powershell/PoshQC/README.md +8 -2
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +2 -315
package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload-powershell.ps1
ADDED
|
@@ -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
|
+
}
|