@wrongstack/tools 0.277.1 → 0.280.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/dist/bash.js +2 -2
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +314 -15
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.js +12 -11
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +12 -11
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/exec-Ca3fnpUh.d.ts +121 -0
- package/dist/exec.d.ts +2 -39
- package/dist/exec.js +322 -11
- package/dist/exec.js.map +1 -1
- package/dist/index.d.ts +46 -3
- package/dist/index.js +629 -153
- package/dist/index.js.map +1 -1
- package/dist/install.js.map +1 -1
- package/dist/json.js +0 -1
- package/dist/json.js.map +1 -1
- package/dist/logs.js.map +1 -1
- package/dist/pack.js +314 -15
- package/dist/pack.js.map +1 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/tool-icons.js +6 -6
- package/dist/tool-icons.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Tool } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Heuristic danger detection for `exec` tool commands.
|
|
5
|
+
*
|
|
6
|
+
* Layered on top of `BLOCKED_ARG_PATTERNS` (which is a hard-deny list for
|
|
7
|
+
* clear sandbox escapes) and `bash-kill-guard.ts` (which protects WrongStack
|
|
8
|
+
* itself from kill). This module assigns a danger level to a command/arg
|
|
9
|
+
* pair so the caller can decide whether to:
|
|
10
|
+
*
|
|
11
|
+
* - 'safe' → execute normally
|
|
12
|
+
* - 'caution' → execute and emit a warning line to the tool output
|
|
13
|
+
* - 'destructive' → route through the existing confirm flow
|
|
14
|
+
* (`execTool.permission === 'confirm'`) instead of
|
|
15
|
+
* hard-deny, so the user can still proceed if intentional
|
|
16
|
+
*
|
|
17
|
+
* Design constraints:
|
|
18
|
+
* - Deterministic: no randomness, no I/O, no time. Same input → same output.
|
|
19
|
+
* - No LLM calls. Patterns are regex / exact-match.
|
|
20
|
+
* - Per-rule `id` so config can override specific rules via
|
|
21
|
+
* `tools.exec.danger.bypass`.
|
|
22
|
+
* - Reasons are human-readable, joined with "; " for the confirm prompt.
|
|
23
|
+
*
|
|
24
|
+
* Caution rules are deliberately permissive — they execute and emit a
|
|
25
|
+
* warning rather than blocking. The rationale: many of these patterns
|
|
26
|
+
* (python -c, sudo, curl | bash) are part of legitimate dev workflows,
|
|
27
|
+
* so a hard deny would block too much. A warning gives the user a
|
|
28
|
+
* chance to notice "wait, I didn't mean to do that" without forcing
|
|
29
|
+
* them to add a config override for every script.
|
|
30
|
+
*/
|
|
31
|
+
type DangerLevel = 'safe' | 'caution' | 'destructive';
|
|
32
|
+
interface DangerAssessment {
|
|
33
|
+
level: DangerLevel;
|
|
34
|
+
reasons: string[];
|
|
35
|
+
/** Stable id of the matched rule, for tests and config-override. */
|
|
36
|
+
matchedRule?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Evaluate the danger level of a (cmd, args) pair.
|
|
40
|
+
*
|
|
41
|
+
* Returns 'safe' if no rule fires, otherwise the highest level among all
|
|
42
|
+
* matching rules. The 'matchedRule' field is the *last* rule that fired
|
|
43
|
+
* (stable, since rules are evaluated in declaration order).
|
|
44
|
+
*
|
|
45
|
+
* Optional `bypass` argument: a set of rule ids that should be SKIPPED
|
|
46
|
+
* even if they would otherwise match. Wired from
|
|
47
|
+
* `config.tools.exec.danger.bypass` (see `ExecDangerConfig` in
|
|
48
|
+
* `@wrongstack/core/src/types/config.ts`). Unknown ids are silently
|
|
49
|
+
* ignored — forward-compat: a rule added in a future version can be
|
|
50
|
+
* referenced before the user upgrades their config schema.
|
|
51
|
+
*
|
|
52
|
+
* This function is the single source of truth for danger classification;
|
|
53
|
+
* it is pure (no side effects) and unit-tested in `danger-detect.test.ts`.
|
|
54
|
+
*/
|
|
55
|
+
declare function detectDanger(cmd: string, args: readonly string[], bypass?: ReadonlySet<string>): DangerAssessment;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Apply the configured exec command policy. Recomputes the effective allowlist
|
|
59
|
+
* as `DEFAULT ∪ allow − deny`. Call once at boot from
|
|
60
|
+
* `config.tools.exec.{allow,deny}`. Idempotent (always rebuilt from defaults).
|
|
61
|
+
*
|
|
62
|
+
* SECURITY: `allow` must originate from TRUSTED config only — the config loader
|
|
63
|
+
* strips `tools.exec.allow` from the untrusted in-project repo config before it
|
|
64
|
+
* reaches here. `deny` is safe from any source (it only narrows).
|
|
65
|
+
*/
|
|
66
|
+
declare function configureExecPolicy(opts?: {
|
|
67
|
+
allow?: readonly string[] | undefined;
|
|
68
|
+
deny?: readonly string[] | undefined;
|
|
69
|
+
}): void;
|
|
70
|
+
/** Reset the exec allowlist to the built-in defaults (tests / re-init). */
|
|
71
|
+
declare function resetExecPolicy(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Apply the configured danger-bypass policy. Each id in `bypass` is
|
|
74
|
+
* added to the effective skip set; duplicates are fine. Idempotent.
|
|
75
|
+
*
|
|
76
|
+
* Call once at boot from `config.tools.exec.danger.bypass`.
|
|
77
|
+
*/
|
|
78
|
+
declare function configureDangerBypass(opts?: {
|
|
79
|
+
bypass?: readonly string[] | undefined;
|
|
80
|
+
}): void;
|
|
81
|
+
/** Reset the danger-bypass set to empty (tests / re-init). */
|
|
82
|
+
declare function resetDangerBypass(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Read-only view of the active bypass set. `detectDanger()` takes a
|
|
85
|
+
* `bypass` argument directly, so consumers should prefer passing this
|
|
86
|
+
* rather than reading the set and matching themselves.
|
|
87
|
+
*/
|
|
88
|
+
declare function getDangerBypass(): ReadonlySet<string>;
|
|
89
|
+
/** Whether `cmd` is currently in the effective exec allowlist. */
|
|
90
|
+
declare function isExecCommandAllowed(cmd: string): boolean;
|
|
91
|
+
/** Snapshot of the effective allowlist (sorted) — for tests / diagnostics. */
|
|
92
|
+
declare function getExecAllowlist(): string[];
|
|
93
|
+
interface ExecInput {
|
|
94
|
+
command: string;
|
|
95
|
+
args?: string[] | undefined;
|
|
96
|
+
cwd?: string | undefined;
|
|
97
|
+
timeout?: number | undefined;
|
|
98
|
+
}
|
|
99
|
+
interface ExecOutput {
|
|
100
|
+
command: string;
|
|
101
|
+
args: string[];
|
|
102
|
+
stdout: string;
|
|
103
|
+
stderr: string;
|
|
104
|
+
exitCode: number;
|
|
105
|
+
truncated: boolean;
|
|
106
|
+
allowed: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Heuristic danger assessment of the (cmd, args) pair. Populated for every
|
|
109
|
+
* call (not just blocked ones) so the UI/TUI can render a banner when the
|
|
110
|
+
* level is 'caution' or 'destructive'. See `_danger-detect.ts` for the
|
|
111
|
+
* rule set.
|
|
112
|
+
*
|
|
113
|
+
* Pre-execution error returns (allowlist miss, circuit breaker, etc.)
|
|
114
|
+
* report `level: 'safe'` because the command never actually ran; the UI
|
|
115
|
+
* should surface the error separately and not also a danger warning.
|
|
116
|
+
*/
|
|
117
|
+
danger: DangerAssessment;
|
|
118
|
+
}
|
|
119
|
+
declare const execTool: Tool<ExecInput, ExecOutput>;
|
|
120
|
+
|
|
121
|
+
export { type DangerAssessment as D, type DangerLevel as a, configureExecPolicy as b, configureDangerBypass as c, detectDanger as d, execTool as e, getExecAllowlist as f, getDangerBypass as g, resetExecPolicy as h, isExecCommandAllowed as i, resetDangerBypass as r };
|
package/dist/exec.d.ts
CHANGED
|
@@ -1,39 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Apply the configured exec command policy. Recomputes the effective allowlist
|
|
5
|
-
* as `DEFAULT ∪ allow − deny`. Call once at boot from
|
|
6
|
-
* `config.tools.exec.{allow,deny}`. Idempotent (always rebuilt from defaults).
|
|
7
|
-
*
|
|
8
|
-
* SECURITY: `allow` must originate from TRUSTED config only — the config loader
|
|
9
|
-
* strips `tools.exec.allow` from the untrusted in-project repo config before it
|
|
10
|
-
* reaches here. `deny` is safe from any source (it only narrows).
|
|
11
|
-
*/
|
|
12
|
-
declare function configureExecPolicy(opts?: {
|
|
13
|
-
allow?: readonly string[] | undefined;
|
|
14
|
-
deny?: readonly string[] | undefined;
|
|
15
|
-
}): void;
|
|
16
|
-
/** Reset the exec allowlist to the built-in defaults (tests / re-init). */
|
|
17
|
-
declare function resetExecPolicy(): void;
|
|
18
|
-
/** Whether `cmd` is currently in the effective exec allowlist. */
|
|
19
|
-
declare function isExecCommandAllowed(cmd: string): boolean;
|
|
20
|
-
/** Snapshot of the effective allowlist (sorted) — for tests / diagnostics. */
|
|
21
|
-
declare function getExecAllowlist(): string[];
|
|
22
|
-
interface ExecInput {
|
|
23
|
-
command: string;
|
|
24
|
-
args?: string[] | undefined;
|
|
25
|
-
cwd?: string | undefined;
|
|
26
|
-
timeout?: number | undefined;
|
|
27
|
-
}
|
|
28
|
-
interface ExecOutput {
|
|
29
|
-
command: string;
|
|
30
|
-
args: string[];
|
|
31
|
-
stdout: string;
|
|
32
|
-
stderr: string;
|
|
33
|
-
exitCode: number;
|
|
34
|
-
truncated: boolean;
|
|
35
|
-
allowed: boolean;
|
|
36
|
-
}
|
|
37
|
-
declare const execTool: Tool<ExecInput, ExecOutput>;
|
|
38
|
-
|
|
39
|
-
export { configureExecPolicy, execTool, getExecAllowlist, isExecCommandAllowed, resetExecPolicy };
|
|
1
|
+
import '@wrongstack/core';
|
|
2
|
+
export { c as configureDangerBypass, b as configureExecPolicy, e as execTool, g as getDangerBypass, f as getExecAllowlist, i as isExecCommandAllowed, r as resetDangerBypass, h as resetExecPolicy } from './exec-Ca3fnpUh.js';
|
package/dist/exec.js
CHANGED
|
@@ -832,6 +832,292 @@ function quoteWin32CmdArg(arg) {
|
|
|
832
832
|
return `"${arg}"`;
|
|
833
833
|
}
|
|
834
834
|
|
|
835
|
+
// src/_danger-detect.ts
|
|
836
|
+
var argHas = (args, value) => args.includes(value);
|
|
837
|
+
var argMatches = (args, re) => args.some((a) => re.test(a));
|
|
838
|
+
var hasShortFlags = (args, letters) => {
|
|
839
|
+
const seen = /* @__PURE__ */ new Set();
|
|
840
|
+
for (const a of args) {
|
|
841
|
+
if (!a.startsWith("-") || a.startsWith("--")) continue;
|
|
842
|
+
for (const ch of a.replace(/^-+/, "")) seen.add(ch);
|
|
843
|
+
}
|
|
844
|
+
return letters.split("").every((l) => seen.has(l));
|
|
845
|
+
};
|
|
846
|
+
var RULES = [
|
|
847
|
+
// ----- rm / rmdir: recursive force delete (any path) -----
|
|
848
|
+
// Note: BLOCKED_ARG_PATTERNS already hard-denies root/home/glob paths,
|
|
849
|
+
// but `rm -rf ./build` is a normal dev workflow that the user might
|
|
850
|
+
// want to do intentionally. We downgrade it to 'destructive' so the
|
|
851
|
+
// confirm prompt can approve.
|
|
852
|
+
{
|
|
853
|
+
id: "rm-recursive",
|
|
854
|
+
level: "destructive",
|
|
855
|
+
test: (cmd, args) => (cmd === "rm" || cmd === "rmdir") && hasShortFlags(args, "rf"),
|
|
856
|
+
reason: "recursive force-delete"
|
|
857
|
+
},
|
|
858
|
+
// ----- Windows PowerShell Remove-Item: -Recurse -Force -----
|
|
859
|
+
{
|
|
860
|
+
id: "powershell-remove-item-recursive-force",
|
|
861
|
+
level: "destructive",
|
|
862
|
+
test: (cmd, args) => {
|
|
863
|
+
if (cmd !== "powershell" && cmd !== "pwsh") return false;
|
|
864
|
+
const hasRecurse = argMatches(args, /^-(?:R|Recurse|Recurse\s)/);
|
|
865
|
+
const hasForce = argHas(args, "-Force") || argHas(args, "-F");
|
|
866
|
+
if (argHas(args, "-WhatIf")) return false;
|
|
867
|
+
return hasRecurse && hasForce;
|
|
868
|
+
},
|
|
869
|
+
reason: "Remove-Item with -Recurse -Force"
|
|
870
|
+
},
|
|
871
|
+
// ----- find -exec / -ok / -execdir -----
|
|
872
|
+
{
|
|
873
|
+
id: "find-exec",
|
|
874
|
+
level: "destructive",
|
|
875
|
+
test: (cmd, args) => {
|
|
876
|
+
if (cmd !== "find") return false;
|
|
877
|
+
return args.some(
|
|
878
|
+
(a) => a === "-exec" || a === "-exec;" || a === "-ok" || a === "-ok;" || a === "-execdir" || a === "-execdir;" || a.startsWith("-exec=") || a.startsWith("-ok=") || a.startsWith("-execdir=")
|
|
879
|
+
);
|
|
880
|
+
},
|
|
881
|
+
reason: "find with -exec/-ok (executes arbitrary command on matches)"
|
|
882
|
+
},
|
|
883
|
+
// ----- git --exec= / --upload-pack= / --receive-pack= -----
|
|
884
|
+
// These run arbitrary commands via the git transport layer.
|
|
885
|
+
{
|
|
886
|
+
id: "git-exec",
|
|
887
|
+
level: "destructive",
|
|
888
|
+
test: (cmd, args) => cmd === "git" && args.some(
|
|
889
|
+
(a) => a.startsWith("--exec=") || a.startsWith("--upload-pack=") || a.startsWith("--receive-pack=") || a === "--exec" || a === "--upload-pack" || a === "--receive-pack"
|
|
890
|
+
),
|
|
891
|
+
reason: "git with --exec/--upload-pack/--receive-pack (runs arbitrary code)"
|
|
892
|
+
},
|
|
893
|
+
// ----- Windows: format / diskpart / bcdedit -----
|
|
894
|
+
{
|
|
895
|
+
id: "win32-format",
|
|
896
|
+
level: "destructive",
|
|
897
|
+
test: (cmd) => cmd === "format" || cmd === "format.exe",
|
|
898
|
+
reason: "format (Windows disk format)"
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
id: "win32-diskpart",
|
|
902
|
+
level: "destructive",
|
|
903
|
+
test: (cmd) => cmd === "diskpart" || cmd === "diskpart.exe",
|
|
904
|
+
reason: "diskpart (Windows partition editor)"
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
id: "win32-bcdedit",
|
|
908
|
+
level: "destructive",
|
|
909
|
+
test: (cmd) => cmd === "bcdedit" || cmd === "bcdedit.exe",
|
|
910
|
+
reason: "bcdedit (Windows boot config editor)"
|
|
911
|
+
},
|
|
912
|
+
// ----- mkfs family -----
|
|
913
|
+
{
|
|
914
|
+
id: "mkfs",
|
|
915
|
+
level: "destructive",
|
|
916
|
+
test: (cmd) => /^mkfs(\.[a-z0-9]+)?$/.test(cmd) || cmd === "mkswap",
|
|
917
|
+
reason: "mkfs (filesystem creation \u2014 destroys existing data)"
|
|
918
|
+
},
|
|
919
|
+
// ----- dd writing to a block device -----
|
|
920
|
+
{
|
|
921
|
+
id: "dd-to-block-device",
|
|
922
|
+
level: "destructive",
|
|
923
|
+
test: (cmd, args) => {
|
|
924
|
+
if (cmd !== "dd") return false;
|
|
925
|
+
return args.some((a) => /of=\/dev\/(sd|hd|nvme|vd|mmcblk|xvd|loop|disk)/.test(a));
|
|
926
|
+
},
|
|
927
|
+
reason: "dd writing to a block device"
|
|
928
|
+
},
|
|
929
|
+
// ----- Secure-erase tools -----
|
|
930
|
+
{
|
|
931
|
+
id: "shred",
|
|
932
|
+
level: "destructive",
|
|
933
|
+
test: (cmd) => cmd === "shred" || cmd === "shred.exe",
|
|
934
|
+
reason: "shred (secure file delete)"
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
id: "wipefs",
|
|
938
|
+
level: "destructive",
|
|
939
|
+
test: (cmd) => cmd === "wipefs" || cmd === "wipefs.exe",
|
|
940
|
+
reason: "wipefs (signature wipe \u2014 destroys filesystem headers)"
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
id: "sdelete",
|
|
944
|
+
level: "destructive",
|
|
945
|
+
test: (cmd) => cmd === "sdelete" || cmd === "sdelete.exe",
|
|
946
|
+
reason: "sdelete (Sysinternals secure delete)"
|
|
947
|
+
},
|
|
948
|
+
// ----- VCS history rewrite (destructive) -----
|
|
949
|
+
// `git push --force` / `-f` rewrites remote history. `--force-with-lease`
|
|
950
|
+
// is the safer variant (checks remote hasn't moved) but still rewrites.
|
|
951
|
+
{
|
|
952
|
+
id: "git-push-force",
|
|
953
|
+
level: "destructive",
|
|
954
|
+
test: (cmd, args) => {
|
|
955
|
+
if (cmd !== "git") return false;
|
|
956
|
+
const pushIdx = args.indexOf("push");
|
|
957
|
+
if (pushIdx < 0) return false;
|
|
958
|
+
for (let i = pushIdx + 1; i < args.length; i++) {
|
|
959
|
+
const a = args[i];
|
|
960
|
+
if (a === "--force" || a === "-f" || a === "--force-with-lease") return true;
|
|
961
|
+
if (!a.startsWith("-") && !a.includes("=")) continue;
|
|
962
|
+
if (a.startsWith("--force")) return true;
|
|
963
|
+
}
|
|
964
|
+
return false;
|
|
965
|
+
},
|
|
966
|
+
reason: "git push with --force / -f (rewrites remote history)"
|
|
967
|
+
},
|
|
968
|
+
// ----- git reset --hard (destructive) -----
|
|
969
|
+
{
|
|
970
|
+
id: "git-reset-hard",
|
|
971
|
+
level: "destructive",
|
|
972
|
+
test: (cmd, args) => cmd === "git" && args.some((a) => a === "--hard" || a.startsWith("--hard=")),
|
|
973
|
+
reason: "git reset --hard (discards working tree + index)"
|
|
974
|
+
},
|
|
975
|
+
// ----- git clean -f / -fd (destructive) -----
|
|
976
|
+
{
|
|
977
|
+
id: "git-clean-force",
|
|
978
|
+
level: "destructive",
|
|
979
|
+
test: (cmd, args) => {
|
|
980
|
+
if (cmd !== "git") return false;
|
|
981
|
+
const cleanIdx = args.indexOf("clean");
|
|
982
|
+
if (cleanIdx < 0) return false;
|
|
983
|
+
return args.slice(cleanIdx + 1).some(
|
|
984
|
+
(a) => a === "-f" || a === "--force" || a.startsWith("-f") || a.startsWith("--force=")
|
|
985
|
+
);
|
|
986
|
+
},
|
|
987
|
+
reason: "git clean -f (deletes untracked files)"
|
|
988
|
+
},
|
|
989
|
+
// ----- package publish (destructive — public, irreversible) -----
|
|
990
|
+
{
|
|
991
|
+
id: "npm-publish",
|
|
992
|
+
level: "destructive",
|
|
993
|
+
test: (cmd, args) => {
|
|
994
|
+
if (!["npm", "pnpm", "yarn", "bun", "cargo"].includes(cmd)) return false;
|
|
995
|
+
return args.includes("publish") || cmd === "cargo" && args.includes("yank");
|
|
996
|
+
},
|
|
997
|
+
reason: "publishing to a public package registry (hard to reverse)"
|
|
998
|
+
},
|
|
999
|
+
// ----- k8s cluster-wide destructive ops (destructive) -----
|
|
1000
|
+
{
|
|
1001
|
+
id: "kubectl-delete-namespace",
|
|
1002
|
+
level: "destructive",
|
|
1003
|
+
test: (cmd, args) => {
|
|
1004
|
+
if (cmd !== "kubectl") return false;
|
|
1005
|
+
const delIdx = args.indexOf("delete");
|
|
1006
|
+
if (delIdx < 0) return false;
|
|
1007
|
+
const after = args.slice(delIdx + 1);
|
|
1008
|
+
return after[0] === "namespace" || after[0] === "ns";
|
|
1009
|
+
},
|
|
1010
|
+
reason: "kubectl delete namespace (deletes all resources in the namespace)"
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
id: "kubectl-drain",
|
|
1014
|
+
level: "destructive",
|
|
1015
|
+
test: (cmd, args) => cmd === "kubectl" && args.includes("drain"),
|
|
1016
|
+
reason: "kubectl drain (evicts pods, marks node unschedulable)"
|
|
1017
|
+
},
|
|
1018
|
+
// ----- inline code evaluation (caution — high false-positive) -----
|
|
1019
|
+
// Common in scripts: `python -c "..."`, `node -e "..."`, `bash -c "..."`.
|
|
1020
|
+
// We tag 'caution' rather than 'destructive' because these are used in
|
|
1021
|
+
// many legitimate one-liners (e.g. `python -c "print(1)"`).
|
|
1022
|
+
{
|
|
1023
|
+
id: "inline-eval",
|
|
1024
|
+
level: "caution",
|
|
1025
|
+
test: (cmd, args) => {
|
|
1026
|
+
if (![
|
|
1027
|
+
"python",
|
|
1028
|
+
"python3",
|
|
1029
|
+
"python2",
|
|
1030
|
+
"node",
|
|
1031
|
+
"bash",
|
|
1032
|
+
"sh",
|
|
1033
|
+
"zsh",
|
|
1034
|
+
"ruby",
|
|
1035
|
+
"perl",
|
|
1036
|
+
"lua"
|
|
1037
|
+
].includes(cmd)) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
return args.some(
|
|
1041
|
+
(a) => a === "-c" || a === "-e" || a === "--eval" || a === "-eval" || a === "-E"
|
|
1042
|
+
);
|
|
1043
|
+
},
|
|
1044
|
+
reason: "inline script evaluation (-c / -e / --eval)"
|
|
1045
|
+
},
|
|
1046
|
+
// ----- pipe-to-shell (caution — well-known exfil pattern) -----
|
|
1047
|
+
// The classic `curl https://... | sh` download-and-run vector. Detected by
|
|
1048
|
+
// looking for a known fetcher followed by a shell sink. We use a simple
|
|
1049
|
+
// substring scan; false positives are limited because both tokens must
|
|
1050
|
+
// appear in the same argv.
|
|
1051
|
+
{
|
|
1052
|
+
id: "pipe-to-shell",
|
|
1053
|
+
level: "caution",
|
|
1054
|
+
test: (_cmd, args) => {
|
|
1055
|
+
const hasFetcher = args.some(
|
|
1056
|
+
(a) => /^(curl|wget|fetch|httpie|http)$/i.test(a) || a.startsWith("curl") || a.startsWith("wget")
|
|
1057
|
+
);
|
|
1058
|
+
const hasShellSink = args.some(
|
|
1059
|
+
(a) => a === "sh" || a === "bash" || a === "zsh" || a === "fish" || a === "pwsh" || a === "powershell" || a.endsWith("/sh") || a.endsWith("/bash") || a.endsWith("/zsh") || a.endsWith("/pwsh")
|
|
1060
|
+
);
|
|
1061
|
+
return hasFetcher && hasShellSink;
|
|
1062
|
+
},
|
|
1063
|
+
reason: "network fetch piped to a shell (download-and-run pattern)"
|
|
1064
|
+
},
|
|
1065
|
+
// ----- privilege escalation (caution) -----
|
|
1066
|
+
{
|
|
1067
|
+
id: "sudo",
|
|
1068
|
+
level: "caution",
|
|
1069
|
+
test: (cmd) => cmd === "sudo" || cmd === "doas",
|
|
1070
|
+
reason: "privilege escalation (sudo / doas)"
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
id: "runas",
|
|
1074
|
+
level: "caution",
|
|
1075
|
+
test: (cmd) => cmd === "runas" || cmd === "runas.exe",
|
|
1076
|
+
reason: "Windows runas (run as different user)"
|
|
1077
|
+
},
|
|
1078
|
+
// ----- world-writable permissions (caution) -----
|
|
1079
|
+
// `chmod 777` is rarely correct. `chmod -R 777` is almost always wrong.
|
|
1080
|
+
// We only flag octal modes; symbolic modes like `chmod o+w` are
|
|
1081
|
+
// left to the operator's discretion.
|
|
1082
|
+
{
|
|
1083
|
+
id: "chmod-world-writable",
|
|
1084
|
+
level: "caution",
|
|
1085
|
+
test: (cmd, args) => {
|
|
1086
|
+
if (cmd !== "chmod") return false;
|
|
1087
|
+
return args.some((a) => /^[0-7]{3,4}$/.test(a) && /7/.test(a));
|
|
1088
|
+
},
|
|
1089
|
+
reason: "chmod with world-writable octal mode (e.g. 777)"
|
|
1090
|
+
}
|
|
1091
|
+
];
|
|
1092
|
+
function detectDanger(cmd, args, bypass) {
|
|
1093
|
+
const reasons = [];
|
|
1094
|
+
let level = "safe";
|
|
1095
|
+
let matchedRule;
|
|
1096
|
+
for (const rule of RULES) {
|
|
1097
|
+
if (bypass?.has(rule.id)) continue;
|
|
1098
|
+
if (!rule.test(cmd, args)) continue;
|
|
1099
|
+
reasons.push(rule.reason);
|
|
1100
|
+
matchedRule = rule.id;
|
|
1101
|
+
if (levelRank(rule.level) > levelRank(level)) {
|
|
1102
|
+
level = rule.level;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (level === "safe") return { level: "safe", reasons: [] };
|
|
1106
|
+
const result = { level, reasons };
|
|
1107
|
+
if (matchedRule !== void 0) result.matchedRule = matchedRule;
|
|
1108
|
+
return result;
|
|
1109
|
+
}
|
|
1110
|
+
function levelRank(level) {
|
|
1111
|
+
switch (level) {
|
|
1112
|
+
case "safe":
|
|
1113
|
+
return 0;
|
|
1114
|
+
case "caution":
|
|
1115
|
+
return 1;
|
|
1116
|
+
case "destructive":
|
|
1117
|
+
return 2;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
835
1121
|
// src/exec.ts
|
|
836
1122
|
var isWin = process.platform === "win32";
|
|
837
1123
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
@@ -1452,6 +1738,21 @@ function configureExecPolicy(opts = {}) {
|
|
|
1452
1738
|
function resetExecPolicy() {
|
|
1453
1739
|
allowedCommands = new Set(DEFAULT_ALLOWED_COMMANDS);
|
|
1454
1740
|
}
|
|
1741
|
+
var dangerBypass = /* @__PURE__ */ new Set();
|
|
1742
|
+
function configureDangerBypass(opts = {}) {
|
|
1743
|
+
const next = /* @__PURE__ */ new Set();
|
|
1744
|
+
for (const id of opts.bypass ?? []) {
|
|
1745
|
+
const trimmed = id.trim();
|
|
1746
|
+
if (trimmed) next.add(trimmed);
|
|
1747
|
+
}
|
|
1748
|
+
dangerBypass = next;
|
|
1749
|
+
}
|
|
1750
|
+
function resetDangerBypass() {
|
|
1751
|
+
dangerBypass = /* @__PURE__ */ new Set();
|
|
1752
|
+
}
|
|
1753
|
+
function getDangerBypass() {
|
|
1754
|
+
return dangerBypass;
|
|
1755
|
+
}
|
|
1455
1756
|
function isExecCommandAllowed(cmd) {
|
|
1456
1757
|
return allowedCommands.has(normalizeCmd(cmd));
|
|
1457
1758
|
}
|
|
@@ -1543,6 +1844,7 @@ function validateArgs(cmd, args) {
|
|
|
1543
1844
|
}
|
|
1544
1845
|
return null;
|
|
1545
1846
|
}
|
|
1847
|
+
var SAFE_DANGER = { level: "safe", reasons: [] };
|
|
1546
1848
|
var execTool = {
|
|
1547
1849
|
name: "exec",
|
|
1548
1850
|
category: "Shell",
|
|
@@ -1587,7 +1889,8 @@ var execTool = {
|
|
|
1587
1889
|
stderr: "Circuit breaker is open \u2014 too many consecutive failures. Use /kill reset to recover.",
|
|
1588
1890
|
exitCode: 1,
|
|
1589
1891
|
truncated: false,
|
|
1590
|
-
allowed: false
|
|
1892
|
+
allowed: false,
|
|
1893
|
+
danger: SAFE_DANGER
|
|
1591
1894
|
};
|
|
1592
1895
|
}
|
|
1593
1896
|
const cmd = input.command.trim();
|
|
@@ -1599,7 +1902,8 @@ var execTool = {
|
|
|
1599
1902
|
stderr: "Empty command",
|
|
1600
1903
|
exitCode: 1,
|
|
1601
1904
|
truncated: false,
|
|
1602
|
-
allowed: false
|
|
1905
|
+
allowed: false,
|
|
1906
|
+
danger: SAFE_DANGER
|
|
1603
1907
|
};
|
|
1604
1908
|
if (!isExecCommandAllowed(cmd)) {
|
|
1605
1909
|
return {
|
|
@@ -1609,11 +1913,13 @@ var execTool = {
|
|
|
1609
1913
|
stderr: `Command "${cmd}" not in allowlist. Add it to your ~/.wrongstack/config.json under "tools": { "exec": { "allow": ["${cmd}"] } }, or use the bash tool for one-off arbitrary commands.`,
|
|
1610
1914
|
exitCode: 1,
|
|
1611
1915
|
truncated: false,
|
|
1612
|
-
allowed: false
|
|
1916
|
+
allowed: false,
|
|
1917
|
+
danger: SAFE_DANGER
|
|
1613
1918
|
};
|
|
1614
1919
|
}
|
|
1615
1920
|
const args = (input.args ?? []).slice(0, MAX_ARGS);
|
|
1616
1921
|
const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS));
|
|
1922
|
+
const danger = detectDanger(cmd, args, dangerBypass);
|
|
1617
1923
|
const argError = validateArgs(cmd, args);
|
|
1618
1924
|
if (argError) {
|
|
1619
1925
|
return {
|
|
@@ -1623,7 +1929,8 @@ var execTool = {
|
|
|
1623
1929
|
stderr: argError,
|
|
1624
1930
|
exitCode: 1,
|
|
1625
1931
|
truncated: false,
|
|
1626
|
-
allowed: false
|
|
1932
|
+
allowed: false,
|
|
1933
|
+
danger
|
|
1627
1934
|
};
|
|
1628
1935
|
}
|
|
1629
1936
|
let cwd;
|
|
@@ -1637,14 +1944,15 @@ var execTool = {
|
|
|
1637
1944
|
stderr: `cwd "${input.cwd ?? ctx.cwd}" resolves outside project root`,
|
|
1638
1945
|
exitCode: 1,
|
|
1639
1946
|
truncated: false,
|
|
1640
|
-
allowed: false
|
|
1947
|
+
allowed: false,
|
|
1948
|
+
danger
|
|
1641
1949
|
};
|
|
1642
1950
|
}
|
|
1643
1951
|
const signal = opts.signal;
|
|
1644
|
-
return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id);
|
|
1952
|
+
return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id, danger);
|
|
1645
1953
|
}
|
|
1646
1954
|
};
|
|
1647
|
-
function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
1955
|
+
function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
1648
1956
|
return new Promise((resolve2) => {
|
|
1649
1957
|
let stdout = "";
|
|
1650
1958
|
let stderr = "";
|
|
@@ -1681,7 +1989,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1681
1989
|
stderr: `spawn failed: ${toErrorMessage(err)}`,
|
|
1682
1990
|
exitCode: 1,
|
|
1683
1991
|
truncated: false,
|
|
1684
|
-
allowed: true
|
|
1992
|
+
allowed: true,
|
|
1993
|
+
danger
|
|
1685
1994
|
});
|
|
1686
1995
|
return;
|
|
1687
1996
|
}
|
|
@@ -1700,7 +2009,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1700
2009
|
stderr: stderrText,
|
|
1701
2010
|
exitCode: isAbort ? 124 : 1,
|
|
1702
2011
|
truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
|
|
1703
|
-
allowed: true
|
|
2012
|
+
allowed: true,
|
|
2013
|
+
danger
|
|
1704
2014
|
});
|
|
1705
2015
|
});
|
|
1706
2016
|
const registry = getProcessRegistry();
|
|
@@ -1748,12 +2058,13 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
1748
2058
|
stderr: normalizeCommandOutput(stderr),
|
|
1749
2059
|
exitCode,
|
|
1750
2060
|
truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
|
|
1751
|
-
allowed: true
|
|
2061
|
+
allowed: true,
|
|
2062
|
+
danger
|
|
1752
2063
|
});
|
|
1753
2064
|
});
|
|
1754
2065
|
});
|
|
1755
2066
|
}
|
|
1756
2067
|
|
|
1757
|
-
export { configureExecPolicy, execTool, getExecAllowlist, isExecCommandAllowed, resetExecPolicy };
|
|
2068
|
+
export { configureDangerBypass, configureExecPolicy, execTool, getDangerBypass, getExecAllowlist, isExecCommandAllowed, resetDangerBypass, resetExecPolicy };
|
|
1758
2069
|
//# sourceMappingURL=exec.js.map
|
|
1759
2070
|
//# sourceMappingURL=exec.js.map
|