@theokit/sdk-tools 0.2.0 → 0.3.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 +6 -0
- package/README.md +2 -0
- package/dist/index.cjs +85 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -14
- package/dist/index.d.ts +25 -14
- package/dist/index.js +85 -117
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 986f340: V3-1 — harden `catastrophicShellReason` to theocode's security-reviewed shell-guard. The `shell_exec` guardrail previously missed 18 of 42 catastrophic commands (empirical probe): `git reset --hard`, `git clean -fd`, secret-file exfiltration (`cat .env | curl`, `tar ~/.aws | nc`), command-substitution / eval RCE (`eval "$(curl)"`, `. <(curl)`, `bash -c "$(curl)"`), `find / -delete` / `-exec rm`, `truncate /dev/sda`, and a range of `rm -rf` targets (`~/sub`, `/usr/local`, `../..`, `$HOME/x`, flags after the operand, any absolute non-scratch path). The rules are ported from theocode's hardened guard (proven by a 42-blocked + 24-allowed corpus at 0 misses / 0 false-positives) as a SUPERSET — the SDK's extra screens (recursive `chmod`/`chown` on a root path, extra block-device families, `//` collapse) are kept, and the segment splitter now also covers `&` and newlines. Reason strings widened to describe each category; the public API (`catastrophicShellReason(cmd): string | null`, `CatastrophicCommandError`) is unchanged. No new dependency.
|
|
8
|
+
|
|
3
9
|
## 0.2.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Built-in tools for `@theokit/sdk` agents. File system, git, subprocess, search-t
|
|
|
4
4
|
|
|
5
5
|
Extracted from `@theokit/sdk@1.7.0` as part of the SDK 2.0 package split.
|
|
6
6
|
|
|
7
|
+
See the [**Theo Harness Capability Map**](../../docs/harness-capability-map.md) for every tool + guard primitive (`buildRepoMap`, `isBlockedIp`, `screenedFetch`, `catastrophicShellReason`, ...) with import paths and examples.
|
|
8
|
+
|
|
7
9
|
## Install
|
|
8
10
|
|
|
9
11
|
```bash
|
package/dist/index.cjs
CHANGED
|
@@ -96,8 +96,8 @@ function isForbiddenPath(input) {
|
|
|
96
96
|
if (first === ".git") return true;
|
|
97
97
|
if (first === "node_modules") return true;
|
|
98
98
|
if (first === ".theo") return true;
|
|
99
|
-
const
|
|
100
|
-
if (LOCK_FILES.has(
|
|
99
|
+
const basename = segments[segments.length - 1];
|
|
100
|
+
if (LOCK_FILES.has(basename)) return true;
|
|
101
101
|
return false;
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -601,128 +601,96 @@ var CatastrophicCommandError = class extends sdk.ConfigurationError {
|
|
|
601
601
|
});
|
|
602
602
|
}
|
|
603
603
|
};
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
"
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const a = t[0];
|
|
626
|
-
const b = t[t.length - 1];
|
|
627
|
-
if (a === '"' && b === '"' || a === "'" && b === "'") return t.slice(1, -1);
|
|
628
|
-
}
|
|
629
|
-
return t;
|
|
630
|
-
}
|
|
631
|
-
function tokenize(s) {
|
|
632
|
-
return s.trim().split(/\s+/).filter(Boolean);
|
|
633
|
-
}
|
|
634
|
-
function splitSegments(cmd) {
|
|
635
|
-
return cmd.split(/&&|\|\||;|\|/);
|
|
636
|
-
}
|
|
637
|
-
function stripPrefixTokens(tokens) {
|
|
638
|
-
let t = tokens;
|
|
639
|
-
let head = t[0];
|
|
640
|
-
while (head !== void 0 && PREFIX_TOKENS.has(basename(unquote(head)))) {
|
|
641
|
-
t = t.slice(1);
|
|
642
|
-
head = t[0];
|
|
643
|
-
}
|
|
644
|
-
return t;
|
|
645
|
-
}
|
|
646
|
-
function operandsOf(tokens) {
|
|
647
|
-
return tokens.slice(1).filter((t) => !t.startsWith("-")).map(unquote);
|
|
648
|
-
}
|
|
649
|
-
var HOME_VAR = /^\$\{?HOME\}?$/;
|
|
650
|
-
function isRootishPath(op) {
|
|
651
|
-
if (op === "~" || op === "*" || op === "." || HOME_VAR.test(op)) return true;
|
|
652
|
-
let collapsed = op.replace(/\/+/g, "/");
|
|
653
|
-
if (collapsed.length > 1 && collapsed.endsWith("/")) collapsed = collapsed.slice(0, -1);
|
|
654
|
-
if (collapsed === "/" || collapsed === "/*" || collapsed === "/.") return true;
|
|
655
|
-
return SYSTEM_DIR.test(collapsed);
|
|
656
|
-
}
|
|
657
|
-
function hasRecursiveForce(tokens) {
|
|
658
|
-
const flags = tokens.slice(1).filter((t) => t.startsWith("-"));
|
|
659
|
-
const recursive = flags.some(
|
|
660
|
-
(f) => f === "--recursive" || !f.startsWith("--") && /[rR]/.test(f)
|
|
661
|
-
);
|
|
662
|
-
const force = flags.some((f) => f === "--force" || !f.startsWith("--") && f.includes("f"));
|
|
604
|
+
function unquote(token) {
|
|
605
|
+
return token.replace(/^(['"])(.*)\1$/, "$2").replace(/^['"]|['"]$/g, "");
|
|
606
|
+
}
|
|
607
|
+
function commandSegments(command) {
|
|
608
|
+
return command.split(/&&|\|\||[;|&\n]/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
609
|
+
}
|
|
610
|
+
function commandArgs(segment, name) {
|
|
611
|
+
const tokens = segment.split(/\s+/);
|
|
612
|
+
let i = 0;
|
|
613
|
+
if (tokens[i] === "sudo") i += 1;
|
|
614
|
+
if (tokens[i] !== name) return null;
|
|
615
|
+
return tokens.slice(i + 1);
|
|
616
|
+
}
|
|
617
|
+
function commandSegmentsNamed(command, name) {
|
|
618
|
+
return commandSegments(command).map((s) => commandArgs(s, name)).filter((args) => args !== null);
|
|
619
|
+
}
|
|
620
|
+
function isRecursiveForce(args) {
|
|
621
|
+
const flags = args.filter((t) => t.startsWith("-")).join(" ");
|
|
622
|
+
if (flags.length === 0) return false;
|
|
623
|
+
const recursive = /-[a-z]*r/i.test(flags) || /--recursive/.test(flags);
|
|
624
|
+
const force = /-[a-z]*f/i.test(flags) || /--force/.test(flags);
|
|
663
625
|
return recursive && force;
|
|
664
626
|
}
|
|
665
|
-
function
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
627
|
+
function isRecursive(args) {
|
|
628
|
+
const flags = args.filter((t) => t.startsWith("-")).join(" ");
|
|
629
|
+
return /-[a-z]*r/i.test(flags) || /--recursive/.test(flags);
|
|
630
|
+
}
|
|
631
|
+
var SAFE_ABSOLUTE_TARGET = /^\/(tmp|var\/tmp)(\/|$)/;
|
|
632
|
+
function targetsDangerousPath(args) {
|
|
633
|
+
const targets = args.filter((token) => token.length > 0 && !token.startsWith("-")).map(unquote);
|
|
634
|
+
return targets.some((raw) => {
|
|
635
|
+
const t = raw.replace(/\/+/g, "/");
|
|
636
|
+
if (t === "/dev/null" || SAFE_ABSOLUTE_TARGET.test(t)) return false;
|
|
637
|
+
return /^\/($|\*)/.test(t) || // "/" or "/*"
|
|
638
|
+
/^\/[^/]/.test(t) || // an absolute path like /etc, /usr/local, /home/user/x
|
|
639
|
+
t === "~" || t.startsWith("~/") || /\$\{?HOME\b\}?/.test(t) || // $HOME or ${HOME}
|
|
640
|
+
t === ".." || t.startsWith("../") || t.includes("/..") || t === "*";
|
|
641
|
+
});
|
|
669
642
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
var ddCheck = (cmd0, tokens) => {
|
|
691
|
-
if (cmd0 !== "dd") return null;
|
|
692
|
-
return tokens.some((t) => unquote(t).startsWith("of=/dev/")) ? "dd writing to a device" : null;
|
|
693
|
-
};
|
|
694
|
-
var gitForceCheck = (cmd0, tokens) => {
|
|
695
|
-
if (cmd0 !== "git" || !tokens.includes("push") || tokens.includes("--force-with-lease")) {
|
|
696
|
-
return null;
|
|
643
|
+
var DEVICE_WIPE = [
|
|
644
|
+
/\bmkfs(\.\w+)?\b/,
|
|
645
|
+
/\bdd\b[^\n]*\bof=\/dev\//,
|
|
646
|
+
/\btruncate\b[^\n]*\s\/dev\//,
|
|
647
|
+
/>\s*\/dev\/(sd|nvme|hd|vd|mmcblk|disk|loop|dm-)/
|
|
648
|
+
];
|
|
649
|
+
var checkRemoteExec = (cmd) => /\b(curl|wget|fetch)\b[^\n]*\|\s*(sudo\s+)?(sh|bash|zsh|dash|python[0-9.]*|node|ruby|perl)\b/i.test(
|
|
650
|
+
cmd
|
|
651
|
+
) || /(\$\(|<\()\s*(sudo\s+)?(curl|wget|fetch)\b/i.test(cmd) || /\b(eval|source)\b[^\n]*\b(curl|wget|fetch)\b/i.test(cmd) ? "executes a remote download (pipe / command-substitution / eval) \u2014 remote code execution" : null;
|
|
652
|
+
var checkDeviceWipe = (cmd) => DEVICE_WIPE.some((re) => re.test(cmd)) ? "writes to a raw block device / formats a disk" : null;
|
|
653
|
+
var checkForkBomb = (cmd) => /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(cmd) ? "fork bomb" : null;
|
|
654
|
+
var checkDestructiveGit = (cmd) => {
|
|
655
|
+
if (/\bgit\b[^\n]*\bpush\b[^\n]*(--force(?!-with-lease)\b|\s-f\b|\s\+\S)/.test(cmd)) {
|
|
656
|
+
return "git force-push (overwrites remote history)";
|
|
657
|
+
}
|
|
658
|
+
if (/\bgit\b[^\n]*\breset\b[^\n]*--hard\b/.test(cmd)) {
|
|
659
|
+
return "git reset --hard (discards committed and working changes)";
|
|
660
|
+
}
|
|
661
|
+
if (/\bgit\b[^\n]*\bclean\b[^\n]*(-[a-z]*f[a-z]*d|-[a-z]*d[a-z]*f)/.test(cmd)) {
|
|
662
|
+
return "git clean -fd (permanently deletes untracked files)";
|
|
697
663
|
}
|
|
698
|
-
|
|
699
|
-
return force ? "git push --force" : null;
|
|
664
|
+
return null;
|
|
700
665
|
};
|
|
701
|
-
var
|
|
702
|
-
|
|
703
|
-
|
|
666
|
+
var checkRm = (cmd) => commandSegmentsNamed(cmd, "rm").some((a) => isRecursiveForce(a) && targetsDangerousPath(a)) ? "recursive force-delete of an absolute, home, or parent path" : null;
|
|
667
|
+
var checkPerm = (cmd) => ["chmod", "chown"].some(
|
|
668
|
+
(name) => commandSegmentsNamed(cmd, name).some((a) => isRecursive(a) && targetsDangerousPath(a))
|
|
669
|
+
) ? "recursive permission change on an absolute, home, or parent path" : null;
|
|
670
|
+
var checkFind = (cmd) => /\bfind\s+(\/\S*|~\S*|\$\{?HOME\}?\S*)\s[^\n]*(-delete\b|-exec\s+rm\b)/.test(cmd) ? "find -delete / -exec rm on an absolute or home path" : null;
|
|
671
|
+
var checkExfiltration = (cmd) => {
|
|
672
|
+
const touchesSecret = /(^|[\s/'"])(\.env(\.\w+)?|id_rsa|id_ed25519|\.ssh(\/|\b)|credentials|\.aws(\/|\b)|\.npmrc)\b/.test(
|
|
673
|
+
cmd
|
|
674
|
+
);
|
|
675
|
+
const sendsNetwork = /\b(curl|wget|nc|netcat|scp|ftp|telnet)\b/.test(cmd) || /\bpython[0-9.]*\s+-m\s+http/.test(cmd);
|
|
676
|
+
return touchesSecret && sendsNetwork ? "sends a secret/credential file over the network (exfiltration)" : null;
|
|
704
677
|
};
|
|
705
|
-
var
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
678
|
+
var CATEGORY_CHECKS = [
|
|
679
|
+
checkRemoteExec,
|
|
680
|
+
checkDeviceWipe,
|
|
681
|
+
checkForkBomb,
|
|
682
|
+
checkDestructiveGit,
|
|
683
|
+
checkRm,
|
|
684
|
+
checkPerm,
|
|
685
|
+
checkFind,
|
|
686
|
+
checkExfiltration
|
|
713
687
|
];
|
|
714
|
-
function catastrophicShellReason(
|
|
715
|
-
|
|
716
|
-
if (
|
|
717
|
-
for (const
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
if (head === void 0) continue;
|
|
721
|
-
const cmd0 = basename(unquote(head));
|
|
722
|
-
for (const check of SEGMENT_CHECKS) {
|
|
723
|
-
const reason = check(cmd0, tokens, seg);
|
|
724
|
-
if (reason) return reason;
|
|
725
|
-
}
|
|
688
|
+
function catastrophicShellReason(command) {
|
|
689
|
+
const cmd = command.trim();
|
|
690
|
+
if (cmd.length === 0) return null;
|
|
691
|
+
for (const check of CATEGORY_CHECKS) {
|
|
692
|
+
const reason = check(cmd);
|
|
693
|
+
if (reason) return reason;
|
|
726
694
|
}
|
|
727
695
|
return null;
|
|
728
696
|
}
|