@fiale-plus/pi-rogue-bundle 0.1.13 → 0.1.15
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/node_modules/@fiale-plus/pi-core/README.md +12 -0
- package/node_modules/@fiale-plus/pi-core/package.json +25 -0
- package/node_modules/@fiale-plus/pi-core/src/context-broker.test.ts +216 -0
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +308 -0
- package/node_modules/@fiale-plus/pi-core/src/index.ts +5 -0
- package/node_modules/@fiale-plus/pi-core/src/paths.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.test.ts +129 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.ts +97 -0
- package/node_modules/@fiale-plus/pi-core/src/storage.ts +39 -0
- package/node_modules/@fiale-plus/pi-core/src/text.test.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/text.ts +14 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +23399 -23399
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.test.ts +19 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.ts +248 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate.test.ts +66 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +53 -12
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +16 -1
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +7 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +4 -37
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/state-versioning.test.ts +227 -0
- package/package.json +4 -2
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { scanShellCommand, type RiskScan } from "./risk.js";
|
|
3
|
+
|
|
4
|
+
describe("scanShellCommand", () => {
|
|
5
|
+
it("returns safe for empty command", () => {
|
|
6
|
+
const result = scanShellCommand("");
|
|
7
|
+
expect(result.safe).toBe(true);
|
|
8
|
+
expect(result.severity).toBe("safe");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns safe for a benign command", () => {
|
|
12
|
+
const result = scanShellCommand("echo hello world");
|
|
13
|
+
expect(result.safe).toBe(true);
|
|
14
|
+
expect(result.severity).toBe("safe");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("detects rm as dangerous", () => {
|
|
18
|
+
const result = scanShellCommand("rm -rf /tmp/foo");
|
|
19
|
+
expect(result.safe).toBe(false);
|
|
20
|
+
expect(result.severity).toBe("danger");
|
|
21
|
+
expect(result.findings.some((f) => f.id === "rm")).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("detects rm only as a word boundary (not inside other words)", () => {
|
|
25
|
+
const result = scanShellCommand("program --remove-all");
|
|
26
|
+
expect(result.safe).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("detects sudo as warning", () => {
|
|
30
|
+
const result = scanShellCommand("sudo apt update");
|
|
31
|
+
expect(result.safe).toBe(false);
|
|
32
|
+
expect(result.severity).toBe("warn");
|
|
33
|
+
expect(result.findings.some((f) => f.id === "sudo")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("detects chmod -R as dangerous", () => {
|
|
37
|
+
const result = scanShellCommand("chmod -R 777 /etc");
|
|
38
|
+
expect(result.safe).toBe(false);
|
|
39
|
+
expect(result.severity).toBe("danger");
|
|
40
|
+
expect(result.findings.some((f) => f.id === "chmod-r")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("detects chown as dangerous", () => {
|
|
44
|
+
const result = scanShellCommand("chown root:root /var");
|
|
45
|
+
expect(result.safe).toBe(false);
|
|
46
|
+
expect(result.severity).toBe("danger");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("detects git push --force as dangerous", () => {
|
|
50
|
+
const result = scanShellCommand("git push --force origin main");
|
|
51
|
+
expect(result.safe).toBe(false);
|
|
52
|
+
expect(result.severity).toBe("danger");
|
|
53
|
+
expect(result.findings.some((f) => f.id === "force-push")).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("detects git push --force-with-lease as dangerous", () => {
|
|
57
|
+
const result = scanShellCommand("git push --force-with-lease origin main");
|
|
58
|
+
expect(result.safe).toBe(false);
|
|
59
|
+
expect(result.severity).toBe("danger");
|
|
60
|
+
expect(result.findings.some((f) => f.id === "force-push")).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("detects curl | sh as dangerous", () => {
|
|
64
|
+
const result = scanShellCommand("curl https://example.com/install.sh | sh");
|
|
65
|
+
expect(result.safe).toBe(false);
|
|
66
|
+
expect(result.severity).toBe("danger");
|
|
67
|
+
expect(result.findings.some((f) => f.id === "curl-shell")).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("detects curl | bash as dangerous", () => {
|
|
71
|
+
const result = scanShellCommand("curl -fsSL https://example.com/install.sh | bash");
|
|
72
|
+
expect(result.safe).toBe(false);
|
|
73
|
+
expect(result.severity).toBe("danger");
|
|
74
|
+
expect(result.findings.some((f) => f.id === "curl-shell")).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("detects mkfs as dangerous", () => {
|
|
78
|
+
const result = scanShellCommand("mkfs.ext4 /dev/sda1");
|
|
79
|
+
expect(result.safe).toBe(false);
|
|
80
|
+
expect(result.severity).toBe("danger");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("detects dd if= as dangerous", () => {
|
|
84
|
+
const result = scanShellCommand("dd if=/dev/zero of=/tmp/out bs=1M count=10");
|
|
85
|
+
expect(result.safe).toBe(false);
|
|
86
|
+
expect(result.severity).toBe("danger");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("detects shutdown as dangerous", () => {
|
|
90
|
+
const result = scanShellCommand("shutdown -h now");
|
|
91
|
+
expect(result.safe).toBe(false);
|
|
92
|
+
expect(result.severity).toBe("danger");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("detects reboot as dangerous", () => {
|
|
96
|
+
const result = scanShellCommand("reboot");
|
|
97
|
+
expect(result.safe).toBe(false);
|
|
98
|
+
expect(result.severity).toBe("danger");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("scans extra fragments", () => {
|
|
102
|
+
const result = scanShellCommand("docker exec -it container bash", ["docker exec"]);
|
|
103
|
+
expect(result.safe).toBe(false);
|
|
104
|
+
expect(result.findings.some((f) => f.id === "extra:docker exec")).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns warn severity when only warnings are found", () => {
|
|
108
|
+
const result = scanShellCommand("sudo echo hello");
|
|
109
|
+
expect(result.safe).toBe(false);
|
|
110
|
+
expect(result.severity).toBe("warn");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns danger severity when danger items are found even with warnings", () => {
|
|
114
|
+
const result = scanShellCommand("sudo rm /tmp/foo");
|
|
115
|
+
expect(result.safe).toBe(false);
|
|
116
|
+
expect(result.severity).toBe("danger");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("handles case-insensitive matching", () => {
|
|
120
|
+
const result = scanShellCommand("RM -rf /");
|
|
121
|
+
expect(result.safe).toBe(false);
|
|
122
|
+
expect(result.findings.some((f) => f.id === "rm")).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("ignores empty extra fragments", () => {
|
|
126
|
+
const result = scanShellCommand("echo foo", ["", " "]);
|
|
127
|
+
expect(result.safe).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export interface RiskFinding {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
severity: "warn" | "danger";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RiskScan {
|
|
8
|
+
safe: boolean;
|
|
9
|
+
severity: "safe" | "warn" | "danger";
|
|
10
|
+
findings: RiskFinding[];
|
|
11
|
+
reason: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_PATTERNS: RiskFinding[] = [
|
|
15
|
+
{ id: "rm", label: "rm", severity: "danger" },
|
|
16
|
+
{ id: "sudo", label: "sudo", severity: "warn" },
|
|
17
|
+
{ id: "chmod-r", label: "chmod -R", severity: "danger" },
|
|
18
|
+
{ id: "chown", label: "chown", severity: "danger" },
|
|
19
|
+
{ id: "mkfs", label: "mkfs", severity: "danger" },
|
|
20
|
+
{ id: "dd", label: "dd if=", severity: "danger" },
|
|
21
|
+
{ id: "shutdown", label: "shutdown", severity: "danger" },
|
|
22
|
+
{ id: "reboot", label: "reboot", severity: "danger" },
|
|
23
|
+
{ id: "force-push", label: "git push --force", severity: "danger" },
|
|
24
|
+
{ id: "curl-shell", label: "curl | sh", severity: "danger" },
|
|
25
|
+
{ id: "wget-shell", label: "wget | sh", severity: "danger" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function contains(command: string, fragment: string): boolean {
|
|
29
|
+
return command.toLowerCase().includes(fragment.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function scanShellCommand(command: string, extraFragments: string[] = []): RiskScan {
|
|
33
|
+
const findings: RiskFinding[] = [];
|
|
34
|
+
const text = command.trim();
|
|
35
|
+
|
|
36
|
+
if (!text) {
|
|
37
|
+
return { safe: true, severity: "safe", findings, reason: "Empty command." };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const pattern of DEFAULT_PATTERNS) {
|
|
41
|
+
switch (pattern.id) {
|
|
42
|
+
case "rm":
|
|
43
|
+
if (/\brm\b/i.test(text)) findings.push(pattern);
|
|
44
|
+
break;
|
|
45
|
+
case "sudo":
|
|
46
|
+
if (/\bsudo\b/i.test(text)) findings.push(pattern);
|
|
47
|
+
break;
|
|
48
|
+
case "chmod-r":
|
|
49
|
+
if (/\bchmod\s+-R\b/i.test(text)) findings.push(pattern);
|
|
50
|
+
break;
|
|
51
|
+
case "chown":
|
|
52
|
+
if (/\bchown\b/i.test(text)) findings.push(pattern);
|
|
53
|
+
break;
|
|
54
|
+
case "mkfs":
|
|
55
|
+
if (/\bmkfs(?:\.[\w-]+)?\b/i.test(text)) findings.push(pattern);
|
|
56
|
+
break;
|
|
57
|
+
case "dd":
|
|
58
|
+
if (/\bdd\s+if=/i.test(text)) findings.push(pattern);
|
|
59
|
+
break;
|
|
60
|
+
case "shutdown":
|
|
61
|
+
if (/\bshutdown\b/i.test(text)) findings.push(pattern);
|
|
62
|
+
break;
|
|
63
|
+
case "reboot":
|
|
64
|
+
if (/\breboot\b/i.test(text)) findings.push(pattern);
|
|
65
|
+
break;
|
|
66
|
+
case "force-push":
|
|
67
|
+
if (/\bgit\s+push\b[\s\S]*--force(?:-with-lease)?/i.test(text)) findings.push(pattern);
|
|
68
|
+
break;
|
|
69
|
+
case "curl-shell":
|
|
70
|
+
if (/\bcurl\b[\s\S]*\|\s*(sh|bash)\b/i.test(text)) findings.push(pattern);
|
|
71
|
+
break;
|
|
72
|
+
case "wget-shell":
|
|
73
|
+
if (/\bwget\b[\s\S]*\|\s*(sh|bash)\b/i.test(text)) findings.push(pattern);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const fragment of extraFragments) {
|
|
79
|
+
if (!fragment.trim()) continue;
|
|
80
|
+
if (contains(text, fragment)) {
|
|
81
|
+
findings.push({ id: `extra:${fragment}`, label: fragment, severity: "danger" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (findings.length === 0) {
|
|
86
|
+
return { safe: true, severity: "safe", findings, reason: "No risky shell fragments found." };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const severity = findings.some((finding) => finding.severity === "danger") ? "danger" : "warn";
|
|
90
|
+
const labels = findings.map((finding) => finding.label).join(", ");
|
|
91
|
+
return {
|
|
92
|
+
safe: false,
|
|
93
|
+
severity,
|
|
94
|
+
findings,
|
|
95
|
+
reason: `Detected risky shell fragment(s): ${labels}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function ensureParent(filePath: string): string {
|
|
5
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
6
|
+
return filePath;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function readText(filePath: string, fallback = ""): string {
|
|
10
|
+
try {
|
|
11
|
+
return readFileSync(filePath, "utf8");
|
|
12
|
+
} catch {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function writeText(filePath: string, text: string): void {
|
|
18
|
+
ensureParent(filePath);
|
|
19
|
+
writeFileSync(filePath, text, "utf8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function appendText(filePath: string, text: string): void {
|
|
23
|
+
ensureParent(filePath);
|
|
24
|
+
appendFileSync(filePath, text, "utf8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readJson<T>(filePath: string, fallback: T): T {
|
|
28
|
+
try {
|
|
29
|
+
const raw = readText(filePath).trim();
|
|
30
|
+
if (!raw) return fallback;
|
|
31
|
+
return JSON.parse(raw) as T;
|
|
32
|
+
} catch {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeJson(filePath: string, value: unknown): void {
|
|
38
|
+
writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { truncate, safeName } from "./text.js";
|
|
3
|
+
|
|
4
|
+
describe("truncate", () => {
|
|
5
|
+
it("returns full text when within limit", () => {
|
|
6
|
+
expect(truncate("hello", 10)).toBe("hello");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("truncates and appends ellipsis when over limit", () => {
|
|
10
|
+
const result = truncate("hello world this is long", 12);
|
|
11
|
+
expect(result).toHaveLength(12);
|
|
12
|
+
expect(result.endsWith("…")).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("handles empty string", () => {
|
|
16
|
+
expect(truncate("", 10)).toBe("");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("safeName", () => {
|
|
21
|
+
it("lowercases and replaces spaces with hyphens", () => {
|
|
22
|
+
expect(safeName("My Feature Branch")).toBe("my-feature-branch");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("replaces special characters with hyphens", () => {
|
|
26
|
+
expect(safeName("feature/auth!!@#")).toBe("feature-auth");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("strips leading/trailing hyphens", () => {
|
|
30
|
+
expect(safeName("--main--")).toBe("main");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("falls back to 'main' for empty input", () => {
|
|
34
|
+
expect(safeName("")).toBe("main");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function truncate(text: string, max: number): string {
|
|
2
|
+
if (text.length <= max) return text;
|
|
3
|
+
return `${text.slice(0, Math.max(0, max - 1))}…`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function safeName(text: string): string {
|
|
7
|
+
return (
|
|
8
|
+
text
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
12
|
+
.replace(/^-+|-+$/g, "") || "main"
|
|
13
|
+
);
|
|
14
|
+
}
|