open-multi-agent-kit 0.90.8 → 0.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +21 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/doctor-provider-cli.d.ts +33 -0
- package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
- package/dist/commands/doctor-provider-cli.js +215 -0
- package/dist/commands/doctor-provider-cli.js.map +1 -0
- package/dist/commands/doctor-provider-transport.d.ts +35 -0
- package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
- package/dist/commands/doctor-provider-transport.js +185 -0
- package/dist/commands/doctor-provider-transport.js.map +1 -0
- package/dist/commands/doctor-provider.d.ts +121 -7
- package/dist/commands/doctor-provider.d.ts.map +1 -1
- package/dist/commands/doctor-provider.js +1295 -102
- package/dist/commands/doctor-provider.js.map +1 -1
- package/dist/commands/session-doctor-cli.d.ts +43 -0
- package/dist/commands/session-doctor-cli.d.ts.map +1 -0
- package/dist/commands/session-doctor-cli.js +494 -0
- package/dist/commands/session-doctor-cli.js.map +1 -0
- package/dist/commands/verify-ci.d.ts +21 -0
- package/dist/commands/verify-ci.d.ts.map +1 -0
- package/dist/commands/verify-ci.js +66 -0
- package/dist/commands/verify-ci.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +140 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1179 -95
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tool-settings.d.ts +35 -0
- package/dist/core/agent-tool-settings.d.ts.map +1 -0
- package/dist/core/agent-tool-settings.js +155 -0
- package/dist/core/agent-tool-settings.js.map +1 -0
- package/dist/core/atomic-session-file.d.ts +14 -0
- package/dist/core/atomic-session-file.d.ts.map +1 -0
- package/dist/core/atomic-session-file.js +104 -0
- package/dist/core/atomic-session-file.js.map +1 -0
- package/dist/core/command-safety.d.ts.map +1 -1
- package/dist/core/command-safety.js +0 -8
- package/dist/core/command-safety.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +6 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +12 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/hysteresis.d.ts +27 -0
- package/dist/core/compaction/hysteresis.d.ts.map +1 -0
- package/dist/core/compaction/hysteresis.js +84 -0
- package/dist/core/compaction/hysteresis.js.map +1 -0
- package/dist/core/compaction/index.d.ts +2 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +2 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts +172 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -0
- package/dist/core/compaction/transaction.js +824 -0
- package/dist/core/compaction/transaction.js.map +1 -0
- package/dist/core/context-budget-governor-v2.d.ts +2 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +2 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
- package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
- package/dist/core/context-budget-reserved-tokens.js +188 -0
- package/dist/core/context-budget-reserved-tokens.js.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
- package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +6 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.d.ts +4 -0
- package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.js +19 -0
- package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +1 -0
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +4 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/durable-file-directory.d.ts +4 -0
- package/dist/core/durable-file-directory.d.ts.map +1 -0
- package/dist/core/durable-file-directory.js +76 -0
- package/dist/core/durable-file-directory.js.map +1 -0
- package/dist/core/durable-file-identity.d.ts +26 -0
- package/dist/core/durable-file-identity.d.ts.map +1 -0
- package/dist/core/durable-file-identity.js +118 -0
- package/dist/core/durable-file-identity.js.map +1 -0
- package/dist/core/durable-file-io.d.ts +29 -0
- package/dist/core/durable-file-io.d.ts.map +1 -0
- package/dist/core/durable-file-io.js +179 -0
- package/dist/core/durable-file-io.js.map +1 -0
- package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
- package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
- package/dist/core/durable-file-lock-cleanup.js +39 -0
- package/dist/core/durable-file-lock-cleanup.js.map +1 -0
- package/dist/core/durable-file-lock-owner.d.ts +18 -0
- package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
- package/dist/core/durable-file-lock-owner.js +78 -0
- package/dist/core/durable-file-lock-owner.js.map +1 -0
- package/dist/core/durable-file-lock-storage.d.ts +35 -0
- package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
- package/dist/core/durable-file-lock-storage.js +263 -0
- package/dist/core/durable-file-lock-storage.js.map +1 -0
- package/dist/core/durable-file-mode.d.ts +8 -0
- package/dist/core/durable-file-mode.d.ts.map +1 -0
- package/dist/core/durable-file-mode.js +41 -0
- package/dist/core/durable-file-mode.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +12 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +46 -37
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +7 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +12 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/index.d.ts +7 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/metrics-sampler.d.ts +20 -0
- package/dist/core/metrics-sampler.d.ts.map +1 -1
- package/dist/core/metrics-sampler.js +52 -0
- package/dist/core/metrics-sampler.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +4 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +16 -7
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/provider-resilience.d.ts +51 -0
- package/dist/core/provider-resilience.d.ts.map +1 -0
- package/dist/core/provider-resilience.js +86 -0
- package/dist/core/provider-resilience.js.map +1 -0
- package/dist/core/redaction.d.ts +8 -0
- package/dist/core/redaction.d.ts.map +1 -0
- package/dist/core/redaction.js +31 -0
- package/dist/core/redaction.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +36 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/run-journal-store.d.ts +53 -0
- package/dist/core/run-journal-store.d.ts.map +1 -0
- package/dist/core/run-journal-store.js +278 -0
- package/dist/core/run-journal-store.js.map +1 -0
- package/dist/core/run-journal.d.ts +172 -0
- package/dist/core/run-journal.d.ts.map +1 -0
- package/dist/core/run-journal.js +1045 -0
- package/dist/core/run-journal.js.map +1 -0
- package/dist/core/sdk.d.ts +7 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +94 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-doctor-plan.d.ts +128 -0
- package/dist/core/session-doctor-plan.d.ts.map +1 -0
- package/dist/core/session-doctor-plan.js +833 -0
- package/dist/core/session-doctor-plan.js.map +1 -0
- package/dist/core/session-file-compaction.d.ts +6 -0
- package/dist/core/session-file-compaction.d.ts.map +1 -0
- package/dist/core/session-file-compaction.js +169 -0
- package/dist/core/session-file-compaction.js.map +1 -0
- package/dist/core/session-file-head.d.ts +10 -0
- package/dist/core/session-file-head.d.ts.map +1 -0
- package/dist/core/session-file-head.js +78 -0
- package/dist/core/session-file-head.js.map +1 -0
- package/dist/core/session-file-persistence.d.ts +30 -0
- package/dist/core/session-file-persistence.d.ts.map +1 -0
- package/dist/core/session-file-persistence.js +106 -0
- package/dist/core/session-file-persistence.js.map +1 -0
- package/dist/core/session-integrity.d.ts +43 -0
- package/dist/core/session-integrity.d.ts.map +1 -0
- package/dist/core/session-integrity.js +420 -0
- package/dist/core/session-integrity.js.map +1 -0
- package/dist/core/session-manager.d.ts +24 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +242 -125
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-owner-lease.d.ts +22 -0
- package/dist/core/session-owner-lease.d.ts.map +1 -0
- package/dist/core/session-owner-lease.js +176 -0
- package/dist/core/session-owner-lease.js.map +1 -0
- package/dist/core/session-path-policy.d.ts +132 -0
- package/dist/core/session-path-policy.d.ts.map +1 -0
- package/dist/core/session-path-policy.js +780 -0
- package/dist/core/session-path-policy.js.map +1 -0
- package/dist/core/session-repair-plan.d.ts +49 -0
- package/dist/core/session-repair-plan.d.ts.map +1 -0
- package/dist/core/session-repair-plan.js +202 -0
- package/dist/core/session-repair-plan.js.map +1 -0
- package/dist/core/session-termination.d.ts +106 -0
- package/dist/core/session-termination.d.ts.map +1 -0
- package/dist/core/session-termination.js +445 -0
- package/dist/core/session-termination.js.map +1 -0
- package/dist/core/settings-manager.d.ts +52 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +54 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +47 -0
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/omp-pure-seams.d.ts +41 -0
- package/dist/core/tools/omp-pure-seams.d.ts.map +1 -0
- package/dist/core/tools/omp-pure-seams.js +142 -0
- package/dist/core/tools/omp-pure-seams.js.map +1 -0
- package/dist/core/tools/omp-seam-runtime.d.ts +100 -0
- package/dist/core/tools/omp-seam-runtime.d.ts.map +1 -0
- package/dist/core/tools/omp-seam-runtime.js +40 -0
- package/dist/core/tools/omp-seam-runtime.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +34 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/verified-bash-adapter.d.ts +36 -0
- package/dist/core/verified-bash-adapter.d.ts.map +1 -0
- package/dist/core/verified-bash-adapter.js +90 -0
- package/dist/core/verified-bash-adapter.js.map +1 -0
- package/dist/guardrails/command-redaction.d.ts +32 -0
- package/dist/guardrails/command-redaction.d.ts.map +1 -0
- package/dist/guardrails/command-redaction.js +776 -0
- package/dist/guardrails/command-redaction.js.map +1 -0
- package/dist/guardrails/evidence-attestation.d.ts +31 -0
- package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
- package/dist/guardrails/evidence-attestation.js +178 -0
- package/dist/guardrails/evidence-attestation.js.map +1 -0
- package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
- package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt-store.js +446 -0
- package/dist/guardrails/evidence-receipt-store.js.map +1 -0
- package/dist/guardrails/evidence-receipt.d.ts +58 -0
- package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt.js +389 -0
- package/dist/guardrails/evidence-receipt.js.map +1 -0
- package/dist/guardrails/evidence-system.d.ts +39 -15
- package/dist/guardrails/evidence-system.d.ts.map +1 -1
- package/dist/guardrails/evidence-system.js +255 -97
- package/dist/guardrails/evidence-system.js.map +1 -1
- package/dist/guardrails/replay-ledger-io.d.ts +6 -0
- package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-io.js +42 -0
- package/dist/guardrails/replay-ledger-io.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
- package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
- package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock.js +135 -0
- package/dist/guardrails/replay-ledger-lock.js.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.js +19 -0
- package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
- package/dist/guardrails/replay-ledger-store.d.ts +32 -0
- package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-store.js +211 -0
- package/dist/guardrails/replay-ledger-store.js.map +1 -0
- package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
- package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
- package/dist/guardrails/shell-command-name-glob.js +233 -0
- package/dist/guardrails/shell-command-name-glob.js.map +1 -0
- package/dist/guardrails/shell-command-option.d.ts +7 -0
- package/dist/guardrails/shell-command-option.d.ts.map +1 -0
- package/dist/guardrails/shell-command-option.js +24 -0
- package/dist/guardrails/shell-command-option.js.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.js +145 -0
- package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
- package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
- package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
- package/dist/guardrails/shell-command-wrapper.js +42 -0
- package/dist/guardrails/shell-command-wrapper.js.map +1 -0
- package/dist/guardrails/verified-executor.d.ts +65 -0
- package/dist/guardrails/verified-executor.d.ts.map +1 -0
- package/dist/guardrails/verified-executor.js +171 -0
- package/dist/guardrails/verified-executor.js.map +1 -0
- package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
- package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
- package/dist/guardrails/workspace-fingerprint.js +634 -0
- package/dist/guardrails/workspace-fingerprint.js.map +1 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +11 -8
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +59 -9
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/omk-aurora-dark.json +80 -0
- package/dist/modes/interactive/theme/omk-aurora-light.json +80 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +5 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +43 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +23 -5
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/types/evidence.d.ts +211 -1
- package/dist/types/evidence.d.ts.map +1 -1
- package/dist/types/evidence.js.map +1 -1
- package/dist/verify-ci.d.ts +3 -0
- package/dist/verify-ci.d.ts.map +1 -0
- package/dist/verify-ci.js +30 -0
- package/dist/verify-ci.js.map +1 -0
- package/docs/adaptorch-preview.md +1 -1
- package/docs/compaction.md +38 -4
- package/docs/extensions.md +2 -0
- package/docs/provider-resilience.md +87 -0
- package/docs/sdk.md +238 -1
- package/docs/settings.md +80 -2
- package/docs/themes.md +2 -0
- package/docs/usage.md +38 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +4 -4
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +22 -13
- package/package.json +6 -5
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
import { StaticShellSyntaxError, normalizeStaticShell as tokenizeStaticShell, } from "./shell-command-tokenizer.js";
|
|
2
|
+
/** Identifies the tokenization rules that produced a persisted redacted command. */
|
|
3
|
+
export const EVIDENCE_COMMAND_REDACTION_POLICY_ID = "omk-command-redaction-v1";
|
|
4
|
+
/** Upper bound for the total number of placeholders in one redacted command. */
|
|
5
|
+
export const MAX_COMMAND_REDACTION_PLACEHOLDERS = 256;
|
|
6
|
+
const REDACTED = "[REDACTED]";
|
|
7
|
+
const KEY_ID_HEX = /^[0-9a-f]{16}$/;
|
|
8
|
+
const NONCE_HEX = /^[0-9a-f]{32}$/;
|
|
9
|
+
const MAC_HEX = /^[0-9a-f]{64}$/;
|
|
10
|
+
const PLACEHOLDER_TYPES = Object.freeze([
|
|
11
|
+
"api-key-header",
|
|
12
|
+
"authorization-header",
|
|
13
|
+
"basic-auth",
|
|
14
|
+
"bearer-token",
|
|
15
|
+
"cli-option-inline",
|
|
16
|
+
"cli-option-value",
|
|
17
|
+
"cookie-header",
|
|
18
|
+
"env-assignment",
|
|
19
|
+
"known-token",
|
|
20
|
+
"url-credential",
|
|
21
|
+
"url-query",
|
|
22
|
+
]);
|
|
23
|
+
const PLACEHOLDER_TYPE_SET = new Set(PLACEHOLDER_TYPES);
|
|
24
|
+
/** All command redaction failures are static-message errors that never echo command data. */
|
|
25
|
+
export class CommandRedactionError extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "CommandRedactionError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Structural command-shape validation (paranoid, own-enumerable-data only)
|
|
33
|
+
// ============================================================================
|
|
34
|
+
function exactObject(value, label, requiredKeys, optionalKeys = []) {
|
|
35
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
36
|
+
throw new CommandRedactionError(`${label} must be an object`);
|
|
37
|
+
}
|
|
38
|
+
const record = value;
|
|
39
|
+
const descriptors = Object.getOwnPropertyDescriptors(record);
|
|
40
|
+
const actual = Object.entries(descriptors)
|
|
41
|
+
.filter(([, descriptor]) => descriptor.enumerable)
|
|
42
|
+
.map(([key]) => key)
|
|
43
|
+
.sort();
|
|
44
|
+
const allowed = new Set([...requiredKeys, ...optionalKeys]);
|
|
45
|
+
const isOwnEnumerableDataProperty = (key) => {
|
|
46
|
+
const descriptor = descriptors[key];
|
|
47
|
+
return descriptor !== undefined && descriptor.enumerable === true && "value" in descriptor;
|
|
48
|
+
};
|
|
49
|
+
if (actual.some((key) => !allowed.has(key)) ||
|
|
50
|
+
requiredKeys.some((key) => !isOwnEnumerableDataProperty(key)) ||
|
|
51
|
+
optionalKeys.some((key) => key in record && !isOwnEnumerableDataProperty(key))) {
|
|
52
|
+
throw new CommandRedactionError(`${label} has an invalid key set`);
|
|
53
|
+
}
|
|
54
|
+
const snapshot = {};
|
|
55
|
+
for (const key of actual) {
|
|
56
|
+
const descriptor = descriptors[key];
|
|
57
|
+
if (descriptor === undefined || !("value" in descriptor)) {
|
|
58
|
+
throw new CommandRedactionError(`${label} has an invalid key set`);
|
|
59
|
+
}
|
|
60
|
+
snapshot[key] = descriptor.value;
|
|
61
|
+
}
|
|
62
|
+
return snapshot;
|
|
63
|
+
}
|
|
64
|
+
function exactArray(value, label) {
|
|
65
|
+
if (!Array.isArray(value))
|
|
66
|
+
throw new CommandRedactionError(`${label} must be an array`);
|
|
67
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
68
|
+
const lengthDescriptor = descriptors.length;
|
|
69
|
+
if (lengthDescriptor === undefined || !("value" in lengthDescriptor)) {
|
|
70
|
+
throw new CommandRedactionError(`${label} has invalid index properties`);
|
|
71
|
+
}
|
|
72
|
+
const rawLength = lengthDescriptor.value;
|
|
73
|
+
if (!Number.isSafeInteger(rawLength) || rawLength < 0) {
|
|
74
|
+
throw new CommandRedactionError(`${label} has invalid index properties`);
|
|
75
|
+
}
|
|
76
|
+
const length = rawLength;
|
|
77
|
+
const snapshot = [];
|
|
78
|
+
for (let index = 0; index < length; index++) {
|
|
79
|
+
const descriptor = descriptors[String(index)];
|
|
80
|
+
if (descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor)) {
|
|
81
|
+
throw new CommandRedactionError(`${label} must use own enumerable data index properties`);
|
|
82
|
+
}
|
|
83
|
+
snapshot.push(descriptor.value);
|
|
84
|
+
}
|
|
85
|
+
const enumerableKeys = Object.entries(descriptors)
|
|
86
|
+
.filter(([, descriptor]) => descriptor.enumerable)
|
|
87
|
+
.map(([key]) => key);
|
|
88
|
+
if (enumerableKeys.length !== length || enumerableKeys.some((key, index) => key !== String(index))) {
|
|
89
|
+
throw new CommandRedactionError(`${label} must use own enumerable data index properties`);
|
|
90
|
+
}
|
|
91
|
+
return snapshot;
|
|
92
|
+
}
|
|
93
|
+
function ownEnumerableDataProperty(record, label, key) {
|
|
94
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
95
|
+
if (descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor)) {
|
|
96
|
+
throw new CommandRedactionError(`${label} has an invalid key set`);
|
|
97
|
+
}
|
|
98
|
+
return descriptor.value;
|
|
99
|
+
}
|
|
100
|
+
function nonEmptyString(value, label) {
|
|
101
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
102
|
+
throw new CommandRedactionError(`${label} must be a non-empty string without NUL bytes`);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function stringWithoutNul(value, label) {
|
|
107
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
108
|
+
throw new CommandRedactionError(`${label} must be a string without NUL bytes`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
function positiveBoundedInteger(value, label, max) {
|
|
113
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > max) {
|
|
114
|
+
throw new CommandRedactionError(`${label} must be a positive integer within the placeholder bound`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
/** Structure-only parse of a command descriptor; performs no credential policy checks. */
|
|
119
|
+
export function parseEvidenceCommandShape(value) {
|
|
120
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
121
|
+
throw new CommandRedactionError("receipt core command must be an object");
|
|
122
|
+
}
|
|
123
|
+
const candidate = value;
|
|
124
|
+
const kind = ownEnumerableDataProperty(candidate, "command descriptor", "kind");
|
|
125
|
+
if (kind === "argv") {
|
|
126
|
+
const raw = exactObject(value, "argv command descriptor", ["kind", "executable", "argv"]);
|
|
127
|
+
const rawArgv = exactArray(raw.argv, "argv command descriptor argv");
|
|
128
|
+
if (!rawArgv.every((argument) => typeof argument === "string")) {
|
|
129
|
+
throw new CommandRedactionError("argv command descriptor argv must be a string array");
|
|
130
|
+
}
|
|
131
|
+
const executable = nonEmptyString(raw.executable, "argv command executable");
|
|
132
|
+
const argv = rawArgv.map((argument, index) => stringWithoutNul(argument, `argv[${index}]`));
|
|
133
|
+
return Object.freeze({ kind: "argv", executable, argv: Object.freeze(argv) });
|
|
134
|
+
}
|
|
135
|
+
if (kind === "shell") {
|
|
136
|
+
const raw = exactObject(value, "shell command descriptor", ["kind", "shell", "script"]);
|
|
137
|
+
return Object.freeze({
|
|
138
|
+
kind: "shell",
|
|
139
|
+
shell: nonEmptyString(raw.shell, "shell command identity"),
|
|
140
|
+
script: nonEmptyString(raw.script, "shell command script"),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
throw new CommandRedactionError("receipt command kind must be argv or shell");
|
|
144
|
+
}
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Placeholder recognition
|
|
147
|
+
// ============================================================================
|
|
148
|
+
function unquote(value) {
|
|
149
|
+
if (value.length >= 2 &&
|
|
150
|
+
((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
|
|
151
|
+
return value.slice(1, -1);
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
function shellCredentialValue(value) {
|
|
156
|
+
const candidate = value.trim();
|
|
157
|
+
if (candidate.length >= 2 &&
|
|
158
|
+
((candidate.startsWith('"') && candidate.endsWith('"')) || (candidate.startsWith("'") && candidate.endsWith("'")))) {
|
|
159
|
+
return candidate.slice(1, -1);
|
|
160
|
+
}
|
|
161
|
+
if (candidate.endsWith('"') || candidate.endsWith("'"))
|
|
162
|
+
return candidate.slice(0, -1);
|
|
163
|
+
return candidate;
|
|
164
|
+
}
|
|
165
|
+
function isCredentialPlaceholder(value) {
|
|
166
|
+
const candidate = unquote(shellCredentialValue(value));
|
|
167
|
+
return (/^\[(?:REDACTED|MASKED)\]$/i.test(candidate) ||
|
|
168
|
+
/^<(?:redacted|masked)>$/i.test(candidate) ||
|
|
169
|
+
/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(candidate) ||
|
|
170
|
+
/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(candidate));
|
|
171
|
+
}
|
|
172
|
+
function isAuthorizationPlaceholder(value) {
|
|
173
|
+
const candidate = shellCredentialValue(value);
|
|
174
|
+
const scheme = /^(?:bearer|basic)\s+(.+)$/i.exec(candidate);
|
|
175
|
+
return isCredentialPlaceholder(scheme?.[1] ?? candidate);
|
|
176
|
+
}
|
|
177
|
+
function isBasicAuthPlaceholder(value) {
|
|
178
|
+
const candidate = shellCredentialValue(value);
|
|
179
|
+
if (isCredentialPlaceholder(candidate))
|
|
180
|
+
return true;
|
|
181
|
+
const separator = candidate.indexOf(":");
|
|
182
|
+
return separator >= 0 && isCredentialPlaceholder(candidate.slice(separator + 1));
|
|
183
|
+
}
|
|
184
|
+
function headerCredentialRule(name) {
|
|
185
|
+
const normalized = name.toLowerCase().replaceAll("_", "-");
|
|
186
|
+
if (normalized === "authorization" || normalized === "proxy-authorization") {
|
|
187
|
+
return { type: "authorization-header", preserveScheme: true };
|
|
188
|
+
}
|
|
189
|
+
if (normalized === "cookie" || normalized === "set-cookie") {
|
|
190
|
+
return { type: "cookie-header", preserveScheme: false };
|
|
191
|
+
}
|
|
192
|
+
if (/^(?:x-)?(?:[a-z0-9]+-)*(?:api-key|api-token|auth-token|access-token|security-token|bearer-token|private-key|token)$/.test(normalized) ||
|
|
193
|
+
/^x-(?:[a-z0-9]+-)+key$/.test(normalized)) {
|
|
194
|
+
return { type: "api-key-header", preserveScheme: false };
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
function isHeaderTokenBoundary(character) {
|
|
199
|
+
return /[\s,;|()"'`]/.test(character);
|
|
200
|
+
}
|
|
201
|
+
function isHeaderTokenStart(text, index) {
|
|
202
|
+
if (index === 0 || isHeaderTokenBoundary(text[index - 1]))
|
|
203
|
+
return true;
|
|
204
|
+
const shortOptionStart = index - 2;
|
|
205
|
+
if (shortOptionStart >= 0 &&
|
|
206
|
+
text.slice(shortOptionStart, index) === "-H" &&
|
|
207
|
+
(shortOptionStart === 0 || isHeaderTokenBoundary(text[shortOptionStart - 1]))) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
const longOptionStart = index - 9;
|
|
211
|
+
return (longOptionStart >= 0 &&
|
|
212
|
+
text.slice(longOptionStart, index) === "--header=" &&
|
|
213
|
+
(longOptionStart === 0 || isHeaderTokenBoundary(text[longOptionStart - 1])));
|
|
214
|
+
}
|
|
215
|
+
function staticShellError() {
|
|
216
|
+
throw new CommandRedactionError("shell command is dynamic or malformed; failing closed");
|
|
217
|
+
}
|
|
218
|
+
function normalizeStaticShell(text) {
|
|
219
|
+
try {
|
|
220
|
+
return tokenizeStaticShell(text);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof StaticShellSyntaxError)
|
|
224
|
+
staticShellError();
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function readCompleteHeaderValue(text, start) {
|
|
229
|
+
let valueStart = start;
|
|
230
|
+
while (valueStart < text.length && /\s/.test(text[valueStart]))
|
|
231
|
+
valueStart++;
|
|
232
|
+
return valueStart === text.length
|
|
233
|
+
? undefined
|
|
234
|
+
: { value: text.slice(valueStart), start: valueStart, end: text.length, scanEnd: text.length };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Walk every credential-bearing header occurrence. Returns whether any literal
|
|
238
|
+
* (non-placeholder) credential exists; optionally collects replacement spans.
|
|
239
|
+
*/
|
|
240
|
+
function scanHeaderCredentials(text, spans) {
|
|
241
|
+
let found = false;
|
|
242
|
+
for (let index = 0; index < text.length; index++) {
|
|
243
|
+
if (!isHeaderTokenStart(text, index))
|
|
244
|
+
continue;
|
|
245
|
+
const name = /^[A-Za-z][A-Za-z0-9_-]*/.exec(text.slice(index))?.[0];
|
|
246
|
+
if (name === undefined)
|
|
247
|
+
continue;
|
|
248
|
+
const rule = headerCredentialRule(name);
|
|
249
|
+
if (rule === undefined)
|
|
250
|
+
continue;
|
|
251
|
+
let separatorIndex = index + name.length;
|
|
252
|
+
while (separatorIndex < text.length && /\s/.test(text[separatorIndex]))
|
|
253
|
+
separatorIndex++;
|
|
254
|
+
if (text[separatorIndex] !== ":" && text[separatorIndex] !== "=")
|
|
255
|
+
continue;
|
|
256
|
+
if (text[separatorIndex] === "=" && name.includes("_"))
|
|
257
|
+
continue;
|
|
258
|
+
const credential = readCompleteHeaderValue(text, separatorIndex + 1);
|
|
259
|
+
if (credential === undefined) {
|
|
260
|
+
found = true;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const placeholder = rule.preserveScheme
|
|
264
|
+
? isAuthorizationPlaceholder(credential.value)
|
|
265
|
+
: isCredentialPlaceholder(credential.value);
|
|
266
|
+
if (!placeholder) {
|
|
267
|
+
found = true;
|
|
268
|
+
const scheme = rule.preserveScheme ? /^(?:bearer|basic)\s+/i.exec(credential.value) : null;
|
|
269
|
+
const redactStart = credential.start + (scheme?.[0].length ?? 0);
|
|
270
|
+
if (redactStart < credential.end)
|
|
271
|
+
spans?.push({ start: redactStart, end: credential.end, type: rule.type });
|
|
272
|
+
}
|
|
273
|
+
index = Math.max(index, credential.scanEnd - 1);
|
|
274
|
+
}
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
// ============================================================================
|
|
278
|
+
// Pattern scanning (shared by detection and tokenization)
|
|
279
|
+
// ============================================================================
|
|
280
|
+
const SECRET_NAME = `(?:token|secret|password|passwd|api[-_]?key|private[-_]?key)`;
|
|
281
|
+
const SECRET_OPTION_NAME = `(?:[A-Za-z0-9]+[-_])*${SECRET_NAME}(?:[-_][A-Za-z0-9]+)*`;
|
|
282
|
+
const envAssignmentPattern = () => new RegExp(String.raw `(?:^|[\s;"'\x60])(?:export\s+)?(?:[A-Za-z0-9_]*${SECRET_NAME}[A-Za-z0-9_]*)\s*[:=]\s*([^\s;]+)`, "dgi");
|
|
283
|
+
const cliOptionPattern = () => new RegExp(String.raw `--(?:${SECRET_OPTION_NAME}|authorization|cookie)(=|\s+)([^\s;]+)`, "dgi");
|
|
284
|
+
const basicAuthPattern = () => /(?:^|[\s;&|()"'`])(?:(?:-[uU])(?:=|\s+)?|--(?:proxy-)?user(?:=|\s+))("[^"\r\n;&|()]*"|'[^'\r\n;&|()]*'|[^\s;&|()]+)/dg;
|
|
285
|
+
const urlCredentialPattern = () => /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:([^/\s@]+)@/dgi;
|
|
286
|
+
const bareBearerPattern = () => /\bbearer\s+([A-Za-z0-9._~+/-]{12,})/dgi;
|
|
287
|
+
const knownTokenPattern = () => /\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9_-]{16,}|github_pat_[A-Za-z0-9_]{20,}|npm_[A-Za-z0-9_-]{16,}|AIza[A-Za-z0-9_-]{35})\b/dg;
|
|
288
|
+
function scanPattern(text, scan, spans, boundary = "shell", shellWords) {
|
|
289
|
+
let found = false;
|
|
290
|
+
for (const match of text.matchAll(scan.pattern)) {
|
|
291
|
+
const value = match[scan.valueGroup];
|
|
292
|
+
if (value === undefined || scan.placeholder?.(value))
|
|
293
|
+
continue;
|
|
294
|
+
found = true;
|
|
295
|
+
if (spans === undefined)
|
|
296
|
+
continue;
|
|
297
|
+
const indices = match.indices?.[scan.valueGroup];
|
|
298
|
+
if (indices === undefined)
|
|
299
|
+
continue;
|
|
300
|
+
const [start, rawEnd] = indices;
|
|
301
|
+
let end = rawEnd;
|
|
302
|
+
if (scan.shellWord && boundary === "shell") {
|
|
303
|
+
const word = shellWords?.find((candidate) => start >= candidate.start && start < candidate.end);
|
|
304
|
+
if (word !== undefined) {
|
|
305
|
+
end = word.end;
|
|
306
|
+
if (scan.shellStopCharacters !== undefined) {
|
|
307
|
+
for (let cursor = start; cursor < end; cursor++) {
|
|
308
|
+
if (scan.shellStopCharacters.includes(text[cursor])) {
|
|
309
|
+
end = cursor;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
staticShellError();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else if (scan.exactArgumentEnd && scan.shellStopCharacters !== undefined) {
|
|
320
|
+
end = start;
|
|
321
|
+
while (end < text.length && !scan.shellStopCharacters.includes(text[end]))
|
|
322
|
+
end++;
|
|
323
|
+
}
|
|
324
|
+
spans.push({ start, end, type: scan.type(match), exactArgumentEnd: scan.exactArgumentEnd });
|
|
325
|
+
}
|
|
326
|
+
return found;
|
|
327
|
+
}
|
|
328
|
+
const ENV_SCAN = {
|
|
329
|
+
pattern: envAssignmentPattern(),
|
|
330
|
+
valueGroup: 1,
|
|
331
|
+
placeholder: isCredentialPlaceholder,
|
|
332
|
+
shellWord: true,
|
|
333
|
+
type: () => "env-assignment",
|
|
334
|
+
};
|
|
335
|
+
const CLI_SCAN = {
|
|
336
|
+
pattern: cliOptionPattern(),
|
|
337
|
+
valueGroup: 2,
|
|
338
|
+
placeholder: isCredentialPlaceholder,
|
|
339
|
+
shellWord: true,
|
|
340
|
+
type: (match) => (match[1] === "=" ? "cli-option-inline" : "cli-option-value"),
|
|
341
|
+
};
|
|
342
|
+
const BASIC_SCAN = {
|
|
343
|
+
pattern: basicAuthPattern(),
|
|
344
|
+
valueGroup: 1,
|
|
345
|
+
placeholder: isBasicAuthPlaceholder,
|
|
346
|
+
shellWord: true,
|
|
347
|
+
type: () => "basic-auth",
|
|
348
|
+
};
|
|
349
|
+
const URL_SCAN = {
|
|
350
|
+
pattern: urlCredentialPattern(),
|
|
351
|
+
valueGroup: 1,
|
|
352
|
+
placeholder: isCredentialPlaceholder,
|
|
353
|
+
type: () => "url-credential",
|
|
354
|
+
};
|
|
355
|
+
const BEARER_SCAN = {
|
|
356
|
+
pattern: bareBearerPattern(),
|
|
357
|
+
valueGroup: 1,
|
|
358
|
+
type: () => "bearer-token",
|
|
359
|
+
};
|
|
360
|
+
const KNOWN_TOKEN_SCAN = {
|
|
361
|
+
pattern: knownTokenPattern(),
|
|
362
|
+
valueGroup: 0,
|
|
363
|
+
type: () => "known-token",
|
|
364
|
+
};
|
|
365
|
+
function withFreshPattern(scan) {
|
|
366
|
+
return { ...scan, pattern: new RegExp(scan.pattern.source, scan.pattern.flags) };
|
|
367
|
+
}
|
|
368
|
+
const QUERY_SECRET_NAME = new RegExp(`^[A-Za-z0-9_.~-]*${SECRET_NAME}[A-Za-z0-9_.~-]*$`, "i");
|
|
369
|
+
function queryPairs(text) {
|
|
370
|
+
const pairs = [];
|
|
371
|
+
for (let index = 0; index < text.length; index++) {
|
|
372
|
+
if (text[index] !== "?" && text[index] !== "&")
|
|
373
|
+
continue;
|
|
374
|
+
const nameStart = index + 1;
|
|
375
|
+
let separator = nameStart;
|
|
376
|
+
while (separator < text.length && !"=&#".includes(text[separator]))
|
|
377
|
+
separator++;
|
|
378
|
+
if (text[separator] !== "=")
|
|
379
|
+
continue;
|
|
380
|
+
const rawName = text.slice(nameStart, separator);
|
|
381
|
+
if (/%(?![0-9a-f]{2})/i.test(rawName))
|
|
382
|
+
staticShellError();
|
|
383
|
+
let name;
|
|
384
|
+
try {
|
|
385
|
+
name = decodeURIComponent(rawName);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
staticShellError();
|
|
389
|
+
}
|
|
390
|
+
if (/%[0-9a-f]{2}|%/i.test(name))
|
|
391
|
+
staticShellError();
|
|
392
|
+
let valueEnd = separator + 1;
|
|
393
|
+
while (valueEnd < text.length && text[valueEnd] !== "&" && text[valueEnd] !== "#")
|
|
394
|
+
valueEnd++;
|
|
395
|
+
pairs.push({ name, rawName, nameStart, valueStart: separator + 1, valueEnd });
|
|
396
|
+
}
|
|
397
|
+
return pairs;
|
|
398
|
+
}
|
|
399
|
+
function scanUrlQueryCredentials(text, spans) {
|
|
400
|
+
let found = false;
|
|
401
|
+
for (const pair of queryPairs(text)) {
|
|
402
|
+
if (!QUERY_SECRET_NAME.test(pair.name))
|
|
403
|
+
continue;
|
|
404
|
+
const value = text.slice(pair.valueStart, pair.valueEnd);
|
|
405
|
+
if (!isCredentialPlaceholder(value)) {
|
|
406
|
+
found = true;
|
|
407
|
+
if (pair.valueStart < pair.valueEnd) {
|
|
408
|
+
spans?.push({ start: pair.valueStart, end: pair.valueEnd, type: "url-query", exactArgumentEnd: true });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return found;
|
|
413
|
+
}
|
|
414
|
+
function scanStaticShellHeaders(shell, spans) {
|
|
415
|
+
let found = false;
|
|
416
|
+
for (const word of shell.words) {
|
|
417
|
+
const local = spans === undefined ? undefined : [];
|
|
418
|
+
if (scanHeaderCredentials(shell.text.slice(word.start, word.end), local))
|
|
419
|
+
found = true;
|
|
420
|
+
for (const span of local ?? [])
|
|
421
|
+
spans?.push({ ...span, start: span.start + word.start, end: span.end + word.start });
|
|
422
|
+
}
|
|
423
|
+
return found;
|
|
424
|
+
}
|
|
425
|
+
function scanStaticShellQueries(shell, spans) {
|
|
426
|
+
let found = false;
|
|
427
|
+
for (const word of shell.words) {
|
|
428
|
+
const local = spans === undefined ? undefined : [];
|
|
429
|
+
if (scanUrlQueryCredentials(shell.text.slice(word.start, word.end), local))
|
|
430
|
+
found = true;
|
|
431
|
+
for (const span of local ?? [])
|
|
432
|
+
spans?.push({ ...span, start: span.start + word.start, end: span.end + word.start });
|
|
433
|
+
}
|
|
434
|
+
return found;
|
|
435
|
+
}
|
|
436
|
+
function mapStaticShellSpans(shell, spans) {
|
|
437
|
+
return spans.map((span) => {
|
|
438
|
+
const first = shell.sources[span.start];
|
|
439
|
+
const last = shell.sources[span.end - 1];
|
|
440
|
+
if (first === undefined || last === undefined)
|
|
441
|
+
staticShellError();
|
|
442
|
+
const end = last.quoteStart !== undefined && last.quoteStart < first.start ? last.contentEnd : last.end;
|
|
443
|
+
return { ...span, start: first.start, end };
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
function credentialAtom(start, value, basic = false, authorization = false) {
|
|
447
|
+
let atomOffset = 0;
|
|
448
|
+
if (authorization)
|
|
449
|
+
atomOffset = /^(?:bearer|basic)\s+/i.exec(value)?.[0].length ?? 0;
|
|
450
|
+
if (basic && !authorization)
|
|
451
|
+
atomOffset = Math.max(0, value.indexOf(":") + 1);
|
|
452
|
+
return { start, end: start + value.length, atomStart: start + atomOffset, atomEnd: start + value.length };
|
|
453
|
+
}
|
|
454
|
+
function headerCredentialRange(word, offset) {
|
|
455
|
+
const start = word.startsWith("-H") ? 2 : word.startsWith("--header=") ? 9 : 0;
|
|
456
|
+
const name = /^[A-Za-z][A-Za-z0-9_-]*/.exec(word.slice(start))?.[0];
|
|
457
|
+
if (name === undefined)
|
|
458
|
+
return undefined;
|
|
459
|
+
const rule = headerCredentialRule(name);
|
|
460
|
+
if (rule === undefined)
|
|
461
|
+
return undefined;
|
|
462
|
+
let separator = start + name.length;
|
|
463
|
+
while (/\s/.test(word[separator] ?? ""))
|
|
464
|
+
separator++;
|
|
465
|
+
if (word[separator] !== ":" && word[separator] !== "=")
|
|
466
|
+
return undefined;
|
|
467
|
+
let valueStart = separator + 1;
|
|
468
|
+
while (/\s/.test(word[valueStart] ?? ""))
|
|
469
|
+
valueStart++;
|
|
470
|
+
return credentialAtom(offset + valueStart, word.slice(valueStart), false, rule.preserveScheme);
|
|
471
|
+
}
|
|
472
|
+
function credentialRanges(shell) {
|
|
473
|
+
const ranges = [];
|
|
474
|
+
for (let index = 0; index < shell.words.length; index++) {
|
|
475
|
+
const word = shell.words[index];
|
|
476
|
+
const text = shell.text.slice(word.start, word.end);
|
|
477
|
+
const previousWord = shell.words[index - 1];
|
|
478
|
+
const previous = previousWord === undefined ? undefined : shell.text.slice(previousWord.start, previousWord.end);
|
|
479
|
+
if (previous !== undefined && (CREDENTIAL_OPTION_PAIR.test(previous) || BASIC_AUTH_OPTION_PAIR.test(previous))) {
|
|
480
|
+
ranges.push(credentialAtom(word.start, text, BASIC_AUTH_OPTION_PAIR.test(previous)));
|
|
481
|
+
}
|
|
482
|
+
const assignment = new RegExp(`^[A-Za-z0-9_]*${SECRET_NAME}[A-Za-z0-9_]*\\s*[:=]\\s*`, "i").exec(text);
|
|
483
|
+
if (assignment !== null)
|
|
484
|
+
ranges.push(credentialAtom(word.start + assignment[0].length, text.slice(assignment[0].length)));
|
|
485
|
+
const option = new RegExp(`^--(?:${SECRET_OPTION_NAME}|authorization|cookie)=`, "i").exec(text);
|
|
486
|
+
if (option !== null)
|
|
487
|
+
ranges.push(credentialAtom(word.start + option[0].length, text.slice(option[0].length)));
|
|
488
|
+
const basic = /^(?:-[uU]=?|--(?:proxy-)?user=)/.exec(text);
|
|
489
|
+
if (basic !== null && basic[0].length < text.length) {
|
|
490
|
+
ranges.push(credentialAtom(word.start + basic[0].length, text.slice(basic[0].length), true));
|
|
491
|
+
}
|
|
492
|
+
const header = headerCredentialRange(text, word.start);
|
|
493
|
+
if (header !== undefined)
|
|
494
|
+
ranges.push(header);
|
|
495
|
+
for (const pair of queryPairs(text)) {
|
|
496
|
+
if (QUERY_SECRET_NAME.test(pair.name)) {
|
|
497
|
+
ranges.push(credentialAtom(word.start + pair.valueStart, text.slice(pair.valueStart, pair.valueEnd)));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
for (const match of text.matchAll(/\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:([^/\s@]+)@/dgi)) {
|
|
501
|
+
const value = match[1];
|
|
502
|
+
const indices = match.indices?.[1];
|
|
503
|
+
if (value !== undefined && indices !== undefined)
|
|
504
|
+
ranges.push(credentialAtom(word.start + indices[0], value));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return ranges;
|
|
508
|
+
}
|
|
509
|
+
function hasDynamicCredentialName(word, previous) {
|
|
510
|
+
const equals = word.indexOf("=");
|
|
511
|
+
if (word.startsWith("--") && word.slice(0, equals < 0 ? word.length : equals).includes("$"))
|
|
512
|
+
return true;
|
|
513
|
+
const headerLike = previous === "-H" || previous === "--header" || word.startsWith("-H") || word.startsWith("--header=");
|
|
514
|
+
if (headerLike) {
|
|
515
|
+
const start = word.startsWith("-H") ? 2 : word.startsWith("--header=") ? 9 : 0;
|
|
516
|
+
const separator = Math.min(...[word.indexOf(":", start), word.indexOf("=", start)].filter((value) => value >= 0));
|
|
517
|
+
if ((separator === Infinity ? word.length : separator) > start && word.slice(start, separator).includes("$"))
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
return queryPairs(word).some((pair) => pair.rawName.includes("$"));
|
|
521
|
+
}
|
|
522
|
+
function assertStaticShellCredentialProvenance(shell) {
|
|
523
|
+
const ranges = credentialRanges(shell);
|
|
524
|
+
for (let index = 0; index < shell.words.length; index++) {
|
|
525
|
+
const word = shell.words[index];
|
|
526
|
+
const previousWord = shell.words[index - 1];
|
|
527
|
+
const previous = previousWord === undefined ? undefined : shell.text.slice(previousWord.start, previousWord.end);
|
|
528
|
+
if (hasDynamicCredentialName(shell.text.slice(word.start, word.end), previous))
|
|
529
|
+
staticShellError();
|
|
530
|
+
}
|
|
531
|
+
for (const range of ranges) {
|
|
532
|
+
const atom = shell.text.slice(range.atomStart, range.atomEnd);
|
|
533
|
+
const variables = shell.variables.filter((variable) => variable.start < range.end && variable.end > range.start);
|
|
534
|
+
if (variables.length === 0) {
|
|
535
|
+
if (atom.includes("$"))
|
|
536
|
+
staticShellError();
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (variables.length !== 1 || variables[0].start !== range.atomStart || variables[0].end !== range.atomEnd) {
|
|
540
|
+
staticShellError();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
for (const variable of shell.variables) {
|
|
544
|
+
const covered = ranges.some((range) => variable.start >= range.atomStart && variable.end <= range.atomEnd);
|
|
545
|
+
if (!covered)
|
|
546
|
+
staticShellError();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function scanCredentialShapedValue(text, boundary, spans, includeBasic = false) {
|
|
550
|
+
const shell = boundary === "shell" ? normalizeStaticShell(text) : undefined;
|
|
551
|
+
if (shell !== undefined)
|
|
552
|
+
assertStaticShellCredentialProvenance(shell);
|
|
553
|
+
const scannedText = shell?.text ?? text;
|
|
554
|
+
const candidates = spans === undefined ? undefined : [];
|
|
555
|
+
let found = false;
|
|
556
|
+
// Classify complete headers and query values first so narrower token forms
|
|
557
|
+
// cannot steal their replacement spans or placeholder types.
|
|
558
|
+
if (shell === undefined) {
|
|
559
|
+
if (scanHeaderCredentials(scannedText, candidates))
|
|
560
|
+
found = true;
|
|
561
|
+
if (scanUrlQueryCredentials(scannedText, candidates))
|
|
562
|
+
found = true;
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
if (scanStaticShellHeaders(shell, candidates))
|
|
566
|
+
found = true;
|
|
567
|
+
if (scanStaticShellQueries(shell, candidates))
|
|
568
|
+
found = true;
|
|
569
|
+
}
|
|
570
|
+
for (const scan of [ENV_SCAN, CLI_SCAN, BEARER_SCAN, KNOWN_TOKEN_SCAN, URL_SCAN]) {
|
|
571
|
+
if (scanPattern(scannedText, withFreshPattern(scan), candidates, boundary, shell?.words))
|
|
572
|
+
found = true;
|
|
573
|
+
}
|
|
574
|
+
if (includeBasic && scanPattern(scannedText, withFreshPattern(BASIC_SCAN), candidates, boundary, shell?.words)) {
|
|
575
|
+
found = true;
|
|
576
|
+
}
|
|
577
|
+
spans?.push(...(shell === undefined ? (candidates ?? []) : mapStaticShellSpans(shell, candidates ?? [])));
|
|
578
|
+
return found;
|
|
579
|
+
}
|
|
580
|
+
function containsCredentialShapedValue(text, boundary) {
|
|
581
|
+
return scanCredentialShapedValue(text, boundary);
|
|
582
|
+
}
|
|
583
|
+
function containsLiteralBasicAuth(text, boundary) {
|
|
584
|
+
const shell = boundary === "shell" ? normalizeStaticShell(text) : undefined;
|
|
585
|
+
return scanPattern(shell?.text ?? text, withFreshPattern(BASIC_SCAN), undefined, boundary, shell?.words);
|
|
586
|
+
}
|
|
587
|
+
const CREDENTIAL_OPTION_PAIR = new RegExp(`^--(?:${SECRET_OPTION_NAME}|authorization|cookie)$`, "i");
|
|
588
|
+
const BASIC_AUTH_OPTION_PAIR = /^(?:-[uU]|--user|--proxy-user)$/;
|
|
589
|
+
const SHELL_EXECUTABLE = /(?:^|[\\/])(?:ba|da|k|z)?sh(?:\.exe)?$/i;
|
|
590
|
+
function argvElementBoundary(executable, previous) {
|
|
591
|
+
const shellScript = SHELL_EXECUTABLE.test(executable) && (previous === "--command" || /^-[A-Za-z]*c[A-Za-z]*$/.test(previous ?? ""));
|
|
592
|
+
return shellScript ? "shell" : "argument";
|
|
593
|
+
}
|
|
594
|
+
function assertCredentialFreeEvidenceCommandImpl(command) {
|
|
595
|
+
const basicAuthText = command.kind === "argv" ? command.argv.join("\n") : command.script;
|
|
596
|
+
const credentialShaped = command.kind === "argv"
|
|
597
|
+
? containsCredentialShapedValue(command.executable, "argument") ||
|
|
598
|
+
command.argv.some((value, index) => containsCredentialShapedValue(value, argvElementBoundary(command.executable, command.argv[index - 1])))
|
|
599
|
+
: containsCredentialShapedValue(command.shell, "argument") ||
|
|
600
|
+
containsCredentialShapedValue(command.script, "shell");
|
|
601
|
+
const literalBasicAuth = containsLiteralBasicAuth(basicAuthText, command.kind === "argv" ? "argument" : "shell");
|
|
602
|
+
if (credentialShaped || literalBasicAuth) {
|
|
603
|
+
throw new CommandRedactionError("command descriptor contains credential-shaped inline data");
|
|
604
|
+
}
|
|
605
|
+
if (command.kind === "argv") {
|
|
606
|
+
for (let index = 0; index < command.argv.length - 1; index++) {
|
|
607
|
+
if (CREDENTIAL_OPTION_PAIR.test(command.argv[index]) && !isCredentialPlaceholder(command.argv[index + 1])) {
|
|
608
|
+
throw new CommandRedactionError("command descriptor contains credential-shaped inline data");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/** Throw without echoing data when a persisted command is secret-bearing or ambiguous. */
|
|
614
|
+
export function assertCredentialFreeEvidenceCommand(command) {
|
|
615
|
+
try {
|
|
616
|
+
assertCredentialFreeEvidenceCommandImpl(command);
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
if (error instanceof CommandRedactionError) {
|
|
620
|
+
throw new CommandRedactionError("command descriptor contains credential-shaped or ambiguous inline data");
|
|
621
|
+
}
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function overlaps(a, b) {
|
|
626
|
+
return a.start < b.end && b.start < a.end;
|
|
627
|
+
}
|
|
628
|
+
/** Replace every detected literal credential in one text with `[REDACTED]`. */
|
|
629
|
+
function redactText(text, boundary, counts) {
|
|
630
|
+
const candidates = [];
|
|
631
|
+
scanCredentialShapedValue(text, boundary, candidates, true);
|
|
632
|
+
const accepted = [];
|
|
633
|
+
for (const candidate of candidates) {
|
|
634
|
+
// In argv elements whitespace is literal data, so the tail after a secret is
|
|
635
|
+
// unsplittable; extend to the element end instead of leaking fragments.
|
|
636
|
+
const span = boundary === "argument" && candidate.exactArgumentEnd !== true
|
|
637
|
+
? { ...candidate, end: text.length }
|
|
638
|
+
: candidate;
|
|
639
|
+
if (span.start >= span.end)
|
|
640
|
+
continue;
|
|
641
|
+
if (accepted.some((existing) => overlaps(existing, span)))
|
|
642
|
+
continue;
|
|
643
|
+
accepted.push(span);
|
|
644
|
+
}
|
|
645
|
+
let redacted = text;
|
|
646
|
+
for (const span of [...accepted].sort((a, b) => b.start - a.start)) {
|
|
647
|
+
redacted = `${redacted.slice(0, span.start)}${REDACTED}${redacted.slice(span.end)}`;
|
|
648
|
+
}
|
|
649
|
+
for (const span of accepted) {
|
|
650
|
+
counts.set(span.type, (counts.get(span.type) ?? 0) + 1);
|
|
651
|
+
}
|
|
652
|
+
return redacted;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Tokenize secret-bearing CLI forms into a persisted-safe representation with
|
|
656
|
+
* `[REDACTED]` placeholders, preserving placeholder count and type. Fails closed
|
|
657
|
+
* when the result is unrepresentable or the placeholder metadata is oversize.
|
|
658
|
+
* The original command is never stored, echoed, or hashed here.
|
|
659
|
+
*/
|
|
660
|
+
export function redactCommandDescriptor(value) {
|
|
661
|
+
const command = parseEvidenceCommandShape(value);
|
|
662
|
+
const counts = new Map();
|
|
663
|
+
let redacted;
|
|
664
|
+
if (command.kind === "shell") {
|
|
665
|
+
redacted = Object.freeze({
|
|
666
|
+
kind: "shell",
|
|
667
|
+
shell: redactText(command.shell, "argument", counts),
|
|
668
|
+
script: redactText(command.script, "shell", counts),
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
const executable = redactText(command.executable, "argument", counts);
|
|
673
|
+
const argv = [...command.argv];
|
|
674
|
+
const consumed = new Set();
|
|
675
|
+
for (let index = 0; index < argv.length - 1; index++) {
|
|
676
|
+
if (CREDENTIAL_OPTION_PAIR.test(argv[index]) && !isCredentialPlaceholder(argv[index + 1])) {
|
|
677
|
+
argv[index + 1] = REDACTED;
|
|
678
|
+
counts.set("cli-option-value", (counts.get("cli-option-value") ?? 0) + 1);
|
|
679
|
+
consumed.add(index + 1);
|
|
680
|
+
}
|
|
681
|
+
else if (BASIC_AUTH_OPTION_PAIR.test(argv[index]) && !isBasicAuthPlaceholder(argv[index + 1])) {
|
|
682
|
+
argv[index + 1] = REDACTED;
|
|
683
|
+
counts.set("basic-auth", (counts.get("basic-auth") ?? 0) + 1);
|
|
684
|
+
consumed.add(index + 1);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
for (let index = 0; index < argv.length; index++) {
|
|
688
|
+
if (consumed.has(index))
|
|
689
|
+
continue;
|
|
690
|
+
argv[index] = redactText(argv[index], argvElementBoundary(command.executable, argv[index - 1]), counts);
|
|
691
|
+
}
|
|
692
|
+
redacted = Object.freeze({ kind: "argv", executable, argv: Object.freeze(argv) });
|
|
693
|
+
}
|
|
694
|
+
let total = 0;
|
|
695
|
+
for (const count of counts.values())
|
|
696
|
+
total += count;
|
|
697
|
+
if (total > MAX_COMMAND_REDACTION_PLACEHOLDERS) {
|
|
698
|
+
throw new CommandRedactionError("redacted command placeholder metadata exceeds the supported bound");
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
assertCredentialFreeEvidenceCommand(redacted);
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
throw new CommandRedactionError("command redaction produced an unrepresentable result; failing closed");
|
|
705
|
+
}
|
|
706
|
+
const placeholders = [...counts.entries()]
|
|
707
|
+
.map(([type, count]) => Object.freeze({ type, count }))
|
|
708
|
+
.sort((a, b) => (a.type < b.type ? -1 : 1));
|
|
709
|
+
return {
|
|
710
|
+
command: redacted,
|
|
711
|
+
summary: Object.freeze({
|
|
712
|
+
policyId: EVIDENCE_COMMAND_REDACTION_POLICY_ID,
|
|
713
|
+
placeholders: Object.freeze(placeholders),
|
|
714
|
+
}),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
// ============================================================================
|
|
718
|
+
// Persisted metadata validation (structurally bounded, fail closed)
|
|
719
|
+
// ============================================================================
|
|
720
|
+
/** Strict bounded parse of persisted command redaction placeholder metadata. */
|
|
721
|
+
export function parseCommandRedactionSummary(value) {
|
|
722
|
+
const raw = exactObject(value, "commandRedaction", ["policyId", "placeholders"]);
|
|
723
|
+
const policyId = nonEmptyString(raw.policyId, "commandRedaction policyId");
|
|
724
|
+
if (policyId.length > 256)
|
|
725
|
+
throw new CommandRedactionError("commandRedaction policyId is too long");
|
|
726
|
+
const rawPlaceholders = exactArray(raw.placeholders, "commandRedaction placeholders");
|
|
727
|
+
if (rawPlaceholders.length > PLACEHOLDER_TYPES.length) {
|
|
728
|
+
throw new CommandRedactionError("commandRedaction placeholder metadata exceeds the supported bound");
|
|
729
|
+
}
|
|
730
|
+
const placeholders = [];
|
|
731
|
+
let total = 0;
|
|
732
|
+
let previousType = "";
|
|
733
|
+
for (const entry of rawPlaceholders) {
|
|
734
|
+
const parsed = exactObject(entry, "commandRedaction placeholder", ["type", "count"]);
|
|
735
|
+
const type = parsed.type;
|
|
736
|
+
if (typeof type !== "string" || !PLACEHOLDER_TYPE_SET.has(type)) {
|
|
737
|
+
throw new CommandRedactionError("commandRedaction placeholder type is invalid");
|
|
738
|
+
}
|
|
739
|
+
if (type <= previousType) {
|
|
740
|
+
throw new CommandRedactionError("commandRedaction placeholder types must be unique and ascending");
|
|
741
|
+
}
|
|
742
|
+
previousType = type;
|
|
743
|
+
const count = positiveBoundedInteger(parsed.count, "commandRedaction placeholder count", MAX_COMMAND_REDACTION_PLACEHOLDERS);
|
|
744
|
+
total += count;
|
|
745
|
+
placeholders.push(Object.freeze({ type: type, count }));
|
|
746
|
+
}
|
|
747
|
+
if (total > MAX_COMMAND_REDACTION_PLACEHOLDERS) {
|
|
748
|
+
throw new CommandRedactionError("commandRedaction placeholder metadata exceeds the supported bound");
|
|
749
|
+
}
|
|
750
|
+
return Object.freeze({ policyId, placeholders: Object.freeze(placeholders) });
|
|
751
|
+
}
|
|
752
|
+
/** Strict bounded parse of a persisted keyed command binding. */
|
|
753
|
+
export function parseCommandHmacBinding(value) {
|
|
754
|
+
const raw = exactObject(value, "commandBinding", ["algorithm", "keyId", "nonce", "mac"]);
|
|
755
|
+
if (raw.algorithm !== "hmac-sha256") {
|
|
756
|
+
throw new CommandRedactionError("commandBinding algorithm must be hmac-sha256");
|
|
757
|
+
}
|
|
758
|
+
if (typeof raw.keyId !== "string" || !KEY_ID_HEX.test(raw.keyId)) {
|
|
759
|
+
throw new CommandRedactionError("commandBinding keyId is invalid");
|
|
760
|
+
}
|
|
761
|
+
if (typeof raw.nonce !== "string" || !NONCE_HEX.test(raw.nonce)) {
|
|
762
|
+
throw new CommandRedactionError("commandBinding nonce is invalid");
|
|
763
|
+
}
|
|
764
|
+
if (typeof raw.mac !== "string" || !MAC_HEX.test(raw.mac)) {
|
|
765
|
+
throw new CommandRedactionError("commandBinding mac is invalid");
|
|
766
|
+
}
|
|
767
|
+
return Object.freeze({
|
|
768
|
+
algorithm: "hmac-sha256",
|
|
769
|
+
keyId: raw.keyId,
|
|
770
|
+
nonce: raw.nonce,
|
|
771
|
+
mac: raw.mac,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
// Compatibility re-exports; implementation lives at the attestation trust boundary.
|
|
775
|
+
export { bindEvidenceCommandHmac, createCommandHmacBinder, } from "./evidence-attestation.js";
|
|
776
|
+
//# sourceMappingURL=command-redaction.js.map
|