sandstream-kit 1.8.0 → 1.11.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/cli.js +80 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/fix.js +3 -1
- package/dist/fix.js.map +1 -1
- package/dist/heal.d.ts +7 -0
- package/dist/heal.js +45 -5
- package/dist/heal.js.map +1 -1
- package/dist/install.d.ts +15 -2
- package/dist/install.js +18 -2
- package/dist/install.js.map +1 -1
- package/dist/memory/hook.js +32 -4
- package/dist/memory/hook.js.map +1 -1
- package/dist/triage-gate.d.ts +51 -0
- package/dist/triage-gate.js +96 -0
- package/dist/triage-gate.js.map +1 -0
- package/dist/triage.d.ts +7 -0
- package/dist/triage.js +31 -5
- package/dist/triage.js.map +1 -1
- package/dist/update-check.d.ts +9 -0
- package/dist/update-check.js +35 -2
- package/dist/update-check.js.map +1 -1
- package/package.json +3 -2
- package/skills/triage/SKILL.md +42 -0
- package/skills/triage/scripts/triage.py +300 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triage gate — kit installs NOTHING untriaged.
|
|
3
|
+
*
|
|
4
|
+
* Every third-party tool/package kit would install (mise scanners pinned as
|
|
5
|
+
* `aqua:`/`npm:`/`pipx:` refs, etc.) is run through `kit triage` first. Only an
|
|
6
|
+
* explicit triage PASS lets an install proceed. This is WATERTIGHT / fail-closed:
|
|
7
|
+
* a WARN, a FAIL, an offline triage, a missing triage script, or a ref we cannot
|
|
8
|
+
* map to a triage target ALL block the install. The only escape is an explicit
|
|
9
|
+
* `--no-triage` override at the command layer, which itself requires elevation
|
|
10
|
+
* and is audit-logged — heal never takes that escape.
|
|
11
|
+
*
|
|
12
|
+
* Core language runtimes (bare mise names like `node`, `pnpm`) are the trusted
|
|
13
|
+
* base: mise installs them from its registry with checksum verification, and
|
|
14
|
+
* they are the language floor kit itself runs on. They pass without a reputation
|
|
15
|
+
* triage. Anything carrying a `scheme:` (a third-party package/binary) is the
|
|
16
|
+
* supply-chain surface and is always triaged.
|
|
17
|
+
*/
|
|
18
|
+
import { runTriage } from "./triage.js";
|
|
19
|
+
/** Core language runtimes managed by mise core — the trusted base, not triaged. */
|
|
20
|
+
export const CORE_RUNTIMES = new Set([
|
|
21
|
+
"node", "nodejs", "pnpm", "npm", "yarn", "bun", "deno",
|
|
22
|
+
"python", "python3", "go", "golang", "ruby", "java", "openjdk",
|
|
23
|
+
"rust", "cargo", "dotnet", "php", "zig", "elixir", "erlang",
|
|
24
|
+
]);
|
|
25
|
+
/** Extract `owner/repo` from `owner/repo`, a github URL, or a `host/owner/repo`. */
|
|
26
|
+
function extractOwnerRepo(rest) {
|
|
27
|
+
const cleaned = rest
|
|
28
|
+
.replace(/^https?:\/\//, "")
|
|
29
|
+
.replace(/^github\.com\//, "")
|
|
30
|
+
.replace(/\.git$/, "");
|
|
31
|
+
const m = cleaned.match(/([^/\s]+\/[^/\s@]+)/);
|
|
32
|
+
return m ? m[1] : null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Map a mise tool identifier to a triage (type, target), mark it a trusted core
|
|
36
|
+
* runtime, or mark it untriageable (→ the gate will fail-closed on it). PURE.
|
|
37
|
+
*/
|
|
38
|
+
export function triageTargetFor(tool) {
|
|
39
|
+
const ref = tool.trim();
|
|
40
|
+
if (!ref.includes(":")) {
|
|
41
|
+
return CORE_RUNTIMES.has(ref.toLowerCase()) ? { kind: "runtime" } : { kind: "untriageable", ref };
|
|
42
|
+
}
|
|
43
|
+
const idx = ref.indexOf(":");
|
|
44
|
+
const scheme = ref.slice(0, idx).toLowerCase();
|
|
45
|
+
const rest = ref.slice(idx + 1);
|
|
46
|
+
if (scheme === "npm")
|
|
47
|
+
return { kind: "triage", type: "npm", target: rest };
|
|
48
|
+
if (scheme === "pip" || scheme === "pipx")
|
|
49
|
+
return { kind: "triage", type: "pip", target: rest };
|
|
50
|
+
// Everything else that names a repo (aqua/ubi/go/github/cargo/...) → repo triage.
|
|
51
|
+
const ownerRepo = extractOwnerRepo(rest);
|
|
52
|
+
if (ownerRepo)
|
|
53
|
+
return { kind: "triage", type: "repo", target: `https://github.com/${ownerRepo}` };
|
|
54
|
+
return { kind: "untriageable", ref };
|
|
55
|
+
}
|
|
56
|
+
const defaultDeps = { runTriage };
|
|
57
|
+
/** First non-empty line of triage output, for a compact block reason. */
|
|
58
|
+
function firstLine(output) {
|
|
59
|
+
return output.split("\n").map((l) => l.trim()).find(Boolean) ?? "no triage output";
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Watertight gate: returns `pass` ONLY for a trusted core runtime or an explicit
|
|
63
|
+
* triage PASS. WARN / FAIL / offline / missing-script / unmappable-ref all return
|
|
64
|
+
* `blocked` (fail-closed).
|
|
65
|
+
*/
|
|
66
|
+
export async function gateInstall(tool, deps = defaultDeps) {
|
|
67
|
+
const t = triageTargetFor(tool);
|
|
68
|
+
if (t.kind === "runtime") {
|
|
69
|
+
return { tool, decision: "pass", reason: "core language runtime (trusted base)" };
|
|
70
|
+
}
|
|
71
|
+
if (t.kind === "untriageable") {
|
|
72
|
+
return {
|
|
73
|
+
tool,
|
|
74
|
+
decision: "blocked",
|
|
75
|
+
reason: `no triage path for "${t.ref}" — cannot verify, refusing to install (fail-closed)`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const res = await deps.runTriage(t.type, t.target);
|
|
79
|
+
if (res.passed) {
|
|
80
|
+
return {
|
|
81
|
+
tool,
|
|
82
|
+
decision: "pass",
|
|
83
|
+
reason: `triage passed (${t.type} ${t.target})`,
|
|
84
|
+
triageType: t.type,
|
|
85
|
+
triageTarget: t.target,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
tool,
|
|
90
|
+
decision: "blocked",
|
|
91
|
+
reason: `triage did not pass (${t.type} ${t.target}): ${firstLine(res.output)}`,
|
|
92
|
+
triageType: t.type,
|
|
93
|
+
triageTarget: t.target,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=triage-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"triage-gate.js","sourceRoot":"","sources":["../src/triage-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,SAAS,EAAsC,MAAM,aAAa,CAAC;AAE5E,mFAAmF;AACnF,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IACnC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IACtD,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS;IAC9D,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ;CAC5D,CAAC,CAAC;AAOH,oFAAoF;AACpF,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,OAAO,GAAG,IAAI;SACjB,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC;SAC3B,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC;SAC7B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACzB,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;IAC/C,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACzB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IACxB,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC;IACpG,CAAC;IACD,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7B,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAC/C,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IAChC,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC3E,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAChG,kFAAkF;IAClF,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,SAAS,EAAE,EAAE,CAAC;IAClG,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC;AACvC,CAAC;AAcD,MAAM,WAAW,GAAa,EAAE,SAAS,EAAE,CAAC;AAE5C,yEAAyE;AACzE,SAAS,SAAS,CAAC,MAAc;IAC/B,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,kBAAkB,CAAC;AACrF,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,OAAiB,WAAW;IAC1E,MAAM,CAAC,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,sCAAsC,EAAE,CAAC;IACpF,CAAC;IACD,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;QAC9B,OAAO;YACL,IAAI;YACJ,QAAQ,EAAE,SAAS;YACnB,MAAM,EAAE,uBAAuB,CAAC,CAAC,GAAG,sDAAsD;SAC3F,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;QACf,OAAO;YACL,IAAI;YACJ,QAAQ,EAAE,MAAM;YAChB,MAAM,EAAE,kBAAkB,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,GAAG;YAC/C,UAAU,EAAE,CAAC,CAAC,IAAI;YAClB,YAAY,EAAE,CAAC,CAAC,MAAM;SACvB,CAAC;IACJ,CAAC;IACD,OAAO;QACL,IAAI;QACJ,QAAQ,EAAE,SAAS;QACnB,MAAM,EAAE,wBAAwB,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,MAAM,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;QAC/E,UAAU,EAAE,CAAC,CAAC,IAAI;QAClB,YAAY,EAAE,CAAC,CAAC,MAAM;KACvB,CAAC;AACJ,CAAC"}
|
package/dist/triage.d.ts
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
* Wraps the Python triage script and integrates with kit's check-security system.
|
|
4
4
|
*/
|
|
5
5
|
import { triageNpmSandbox, type SandboxResult } from "./triage-sandbox.js";
|
|
6
|
+
/**
|
|
7
|
+
* Self-bootstrap the gate: copy the triage skill kit ships with into
|
|
8
|
+
* ~/.claude/skills/triage. Copying kit's OWN bundled, provenance-published asset
|
|
9
|
+
* is not a third-party install, so it does not itself need triage. Returns true
|
|
10
|
+
* if the script is in place afterwards.
|
|
11
|
+
*/
|
|
12
|
+
export declare function installBundledTriageSkill(targetDir?: string): Promise<boolean>;
|
|
6
13
|
export type TriageType = "docker" | "npm" | "pip" | "repo" | "skill" | "all" | "tools";
|
|
7
14
|
export { triageNpmSandbox, type SandboxResult };
|
|
8
15
|
export interface TriageResult {
|
package/dist/triage.js
CHANGED
|
@@ -2,14 +2,40 @@
|
|
|
2
2
|
* Triage — Security evaluation for open source packages, Docker images, skills, and repos.
|
|
3
3
|
* Wraps the Python triage script and integrates with kit's check-security system.
|
|
4
4
|
*/
|
|
5
|
-
import { access } from "node:fs/promises";
|
|
6
|
-
import {
|
|
5
|
+
import { access, cp, mkdir } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
7
9
|
import { triageNpmSandbox } from "./triage-sandbox.js";
|
|
8
10
|
import { exec } from "./utils/exec.js";
|
|
9
|
-
const
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
/** Where kit looks for the triage skill at runtime. */
|
|
13
|
+
const TRIAGE_SKILL_DIR = resolve(homedir(), ".claude/skills/triage");
|
|
14
|
+
const TRIAGE_SCRIPT = resolve(TRIAGE_SKILL_DIR, "scripts/triage.py");
|
|
15
|
+
/** The copy kit ships in its own package (published via package.json "files"). */
|
|
16
|
+
const BUNDLED_TRIAGE_SKILL = resolve(__dirname, "..", "skills", "triage");
|
|
17
|
+
/**
|
|
18
|
+
* Self-bootstrap the gate: copy the triage skill kit ships with into
|
|
19
|
+
* ~/.claude/skills/triage. Copying kit's OWN bundled, provenance-published asset
|
|
20
|
+
* is not a third-party install, so it does not itself need triage. Returns true
|
|
21
|
+
* if the script is in place afterwards.
|
|
22
|
+
*/
|
|
23
|
+
export async function installBundledTriageSkill(targetDir = TRIAGE_SKILL_DIR) {
|
|
24
|
+
try {
|
|
25
|
+
await mkdir(dirname(targetDir), { recursive: true });
|
|
26
|
+
await cp(BUNDLED_TRIAGE_SKILL, targetDir, { recursive: true });
|
|
27
|
+
await access(resolve(targetDir, "scripts/triage.py"));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
10
34
|
export { triageNpmSandbox };
|
|
11
35
|
/**
|
|
12
|
-
*
|
|
36
|
+
* Ensure the triage script is present. If it is missing, self-bootstrap from the
|
|
37
|
+
* copy kit ships, so the watertight gate works on a fresh machine without a
|
|
38
|
+
* manual "copy the triage skill" step.
|
|
13
39
|
*/
|
|
14
40
|
async function ensureTriageScript() {
|
|
15
41
|
try {
|
|
@@ -17,7 +43,7 @@ async function ensureTriageScript() {
|
|
|
17
43
|
return true;
|
|
18
44
|
}
|
|
19
45
|
catch {
|
|
20
|
-
return
|
|
46
|
+
return installBundledTriageSkill();
|
|
21
47
|
}
|
|
22
48
|
}
|
|
23
49
|
/**
|
package/dist/triage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"triage.js","sourceRoot":"","sources":["../src/triage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"triage.js","sourceRoot":"","sources":["../src/triage.ts"],"names":[],"mappings":"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"}
|
package/dist/update-check.d.ts
CHANGED
|
@@ -9,5 +9,14 @@ export interface UpdateInfo {
|
|
|
9
9
|
* Never throws — all errors are caught silently.
|
|
10
10
|
*/
|
|
11
11
|
export declare function checkForUpdate(currentVersion: string): Promise<UpdateInfo | null>;
|
|
12
|
+
/** Current kit version from the installed package.json (sync, fail-safe). */
|
|
13
|
+
export declare function getKitVersionSync(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Cache-only update check — NO network. For hot paths (the Claude Code hooks that
|
|
16
|
+
* run on every prompt): reads the cache the post-command banner / `kit check`
|
|
17
|
+
* already refresh, never fetches. Returns null on cache miss, error, suppression,
|
|
18
|
+
* or when current is already latest.
|
|
19
|
+
*/
|
|
20
|
+
export declare function readCachedUpdateSync(currentVersion: string): UpdateInfo | null;
|
|
12
21
|
/** Format and print the update notice after command output. */
|
|
13
22
|
export declare function printUpdateNotice(info: UpdateInfo): void;
|
package/dist/update-check.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { homedir } from "node:os";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
4
7
|
const PACKAGE_NAME = "sandstream-kit";
|
|
5
8
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
6
9
|
const CACHE_DIR = join(homedir(), ".kit");
|
|
@@ -63,6 +66,36 @@ export async function checkForUpdate(currentVersion) {
|
|
|
63
66
|
return null;
|
|
64
67
|
}
|
|
65
68
|
}
|
|
69
|
+
/** Current kit version from the installed package.json (sync, fail-safe). */
|
|
70
|
+
export function getKitVersionSync() {
|
|
71
|
+
try {
|
|
72
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
|
|
73
|
+
return pkg.version ?? "unknown";
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Cache-only update check — NO network. For hot paths (the Claude Code hooks that
|
|
81
|
+
* run on every prompt): reads the cache the post-command banner / `kit check`
|
|
82
|
+
* already refresh, never fetches. Returns null on cache miss, error, suppression,
|
|
83
|
+
* or when current is already latest.
|
|
84
|
+
*/
|
|
85
|
+
export function readCachedUpdateSync(currentVersion) {
|
|
86
|
+
try {
|
|
87
|
+
if (process.env.KIT_NO_UPDATE_CHECK === "1")
|
|
88
|
+
return null;
|
|
89
|
+
const cache = JSON.parse(readFileSync(CACHE_FILE, "utf8"));
|
|
90
|
+
if (cache?.latestVersion && isNewer(cache.latestVersion, currentVersion)) {
|
|
91
|
+
return { available: true, latest: cache.latestVersion, current: currentVersion };
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
66
99
|
/** Returns true if `latest` is strictly newer than `current` (semver comparison). */
|
|
67
100
|
function isNewer(latest, current) {
|
|
68
101
|
try {
|
|
@@ -86,6 +119,6 @@ export function printUpdateNotice(info) {
|
|
|
86
119
|
const yellow = "\x1b[33m";
|
|
87
120
|
const cyan = "\x1b[36m";
|
|
88
121
|
console.log(`\n ${dim}╰─${reset} ${yellow}Update available${reset}: ${dim}${info.current}${reset} → ${cyan}${info.latest}${reset} ` +
|
|
89
|
-
`${dim}
|
|
122
|
+
`${dim}run ${reset}${cyan}kit upgrade --self${reset}${dim} (triages before installing)${reset}`);
|
|
90
123
|
}
|
|
91
124
|
//# sourceMappingURL=update-check.js.map
|
package/dist/update-check.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-check.js","sourceRoot":"","sources":["../src/update-check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"update-check.js","sourceRoot":"","sources":["../src/update-check.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sandstream-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "developer kit. zero LLM, local-first, multi-vault. one command from git clone to working dev environment.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"funding": "https://buymeacoffee.com/sandstream",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
-
"README.md"
|
|
22
|
+
"README.md",
|
|
23
|
+
"skills"
|
|
23
24
|
],
|
|
24
25
|
"publishConfig": {
|
|
25
26
|
"access": "public"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: triage
|
|
3
|
+
description: "Security-triage a dependency before installing it."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Triage
|
|
7
|
+
|
|
8
|
+
Deterministic, zero-LLM pre-install security evaluation. kit shells to
|
|
9
|
+
`scripts/triage.py` from its watertight install gate; only an explicit
|
|
10
|
+
`TRIAGE PASSED` lets an install proceed.
|
|
11
|
+
|
|
12
|
+
## Run
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
python3 scripts/triage.py <type> <target>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| type | target | checks |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `npm` | package name | existence, deprecation, age, maintainer count |
|
|
21
|
+
| `pip` | package name | existence, yanked, age, license |
|
|
22
|
+
| `repo` | `owner/repo` or a GitHub URL | archived/disabled, maintenance, license, age |
|
|
23
|
+
| `docker` | image | existence, freshness, publisher |
|
|
24
|
+
| `skill` | path or name | validate a local `SKILL.md` (frontmatter, no secrets), else repo-check |
|
|
25
|
+
| `tools` | (none) | list available checks |
|
|
26
|
+
|
|
27
|
+
## Output contract
|
|
28
|
+
|
|
29
|
+
- Prints `Health score: N/100`, `Critical issues: N`, `Warnings: N`.
|
|
30
|
+
- Prints `TRIAGE PASSED` when there are **zero critical issues**; warnings are
|
|
31
|
+
surfaced and scored but do not, by themselves, withhold a pass (criticals do).
|
|
32
|
+
- **Fail-closed:** if a registry cannot be reached (offline, timeout, HTTP error)
|
|
33
|
+
that is a CRITICAL ("cannot verify"), so the pass is withheld and kit blocks the
|
|
34
|
+
install. Set `GITHUB_TOKEN` to avoid GitHub rate limits on `repo` checks.
|
|
35
|
+
|
|
36
|
+
## Rules
|
|
37
|
+
|
|
38
|
+
- Stdlib only (urllib). No third-party deps, no network calls other than the
|
|
39
|
+
target registry/API. No LLM, no randomness: same input + same upstream state
|
|
40
|
+
yields the same verdict.
|
|
41
|
+
- Keep new checks deterministic and offline-degrading (a check that cannot run is
|
|
42
|
+
a critical, never a silent pass).
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""kit triage — deterministic, zero-LLM pre-install security evaluation.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
triage.py <type> <target>
|
|
6
|
+
type: npm | pip | repo | docker | skill | tools | all
|
|
7
|
+
|
|
8
|
+
kit (src/triage.ts) shells to this script and reads its STDOUT:
|
|
9
|
+
- the line "TRIAGE PASSED" must be present for kit to treat the target as safe;
|
|
10
|
+
- "Health score: N/100", "Critical issues: N", "Warnings: N" are parsed for the
|
|
11
|
+
structured summary.
|
|
12
|
+
|
|
13
|
+
Design contract (matches kit's watertight gate):
|
|
14
|
+
- Deterministic. No LLM, no randomness. Same input + same upstream state => same verdict.
|
|
15
|
+
- Dependency-light. Python stdlib only (urllib), so the skill is portable.
|
|
16
|
+
- Fail-closed. If a registry cannot be reached (offline, timeout, error), that is a
|
|
17
|
+
CRITICAL ("cannot verify") and "TRIAGE PASSED" is withheld, so kit blocks the install.
|
|
18
|
+
- PASS rule: "TRIAGE PASSED" is printed when there are zero CRITICAL issues. Warnings
|
|
19
|
+
are surfaced and scored but do not, by themselves, withhold PASS (criticals do).
|
|
20
|
+
|
|
21
|
+
Exit code is always 0 on a completed evaluation; kit reads the text, not the code.
|
|
22
|
+
"""
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import urllib.error
|
|
27
|
+
import urllib.parse
|
|
28
|
+
import urllib.request
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
|
|
31
|
+
TIMEOUT = 15
|
|
32
|
+
UA = {"User-Agent": "kit-triage/1.0 (+https://github.com/sandstream/kit)"}
|
|
33
|
+
NEW_DAYS = 30 # younger than this => warning (insufficient track record)
|
|
34
|
+
ABANDONED_DAYS = 730 # no release/push in this long => warning
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_json(url, headers=None):
|
|
38
|
+
h = dict(UA)
|
|
39
|
+
if headers:
|
|
40
|
+
h.update(headers)
|
|
41
|
+
req = urllib.request.Request(url, headers=h)
|
|
42
|
+
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
|
43
|
+
return json.load(r), r.status
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _days_since(iso):
|
|
47
|
+
"""Days since an ISO-8601 timestamp, or None if unparseable."""
|
|
48
|
+
if not iso:
|
|
49
|
+
return None
|
|
50
|
+
try:
|
|
51
|
+
s = iso.replace("Z", "+00:00")
|
|
52
|
+
dt = datetime.fromisoformat(s)
|
|
53
|
+
if dt.tzinfo is None:
|
|
54
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
55
|
+
return (datetime.now(timezone.utc) - dt).days
|
|
56
|
+
except ValueError:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Report:
|
|
61
|
+
def __init__(self, ttype, target):
|
|
62
|
+
self.ttype = ttype
|
|
63
|
+
self.target = target
|
|
64
|
+
self.criticals = []
|
|
65
|
+
self.warnings = []
|
|
66
|
+
self.facts = []
|
|
67
|
+
|
|
68
|
+
def critical(self, m):
|
|
69
|
+
self.criticals.append(m)
|
|
70
|
+
|
|
71
|
+
def warn(self, m):
|
|
72
|
+
self.warnings.append(m)
|
|
73
|
+
|
|
74
|
+
def fact(self, m):
|
|
75
|
+
self.facts.append(m)
|
|
76
|
+
|
|
77
|
+
def emit(self):
|
|
78
|
+
score = max(0, 100 - 45 * len(self.criticals) - 12 * len(self.warnings))
|
|
79
|
+
print(f"Triage: {self.ttype} {self.target}")
|
|
80
|
+
print("-" * 50)
|
|
81
|
+
for f in self.facts:
|
|
82
|
+
print(f" . {f}")
|
|
83
|
+
for w in self.warnings:
|
|
84
|
+
print(f" ! WARNING: {w}")
|
|
85
|
+
for c in self.criticals:
|
|
86
|
+
print(f" x CRITICAL: {c}")
|
|
87
|
+
print()
|
|
88
|
+
print(f"Health score: {score}/100")
|
|
89
|
+
print(f"Critical issues: {len(self.criticals)}")
|
|
90
|
+
print(f"Warnings: {len(self.warnings)}")
|
|
91
|
+
if not self.criticals:
|
|
92
|
+
print("TRIAGE PASSED")
|
|
93
|
+
else:
|
|
94
|
+
print("TRIAGE FAILED")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def triage_npm(rep):
|
|
98
|
+
pkg = rep.target
|
|
99
|
+
url = f"https://registry.npmjs.org/{urllib.parse.quote(pkg, safe='@/')}"
|
|
100
|
+
try:
|
|
101
|
+
data, _ = _get_json(url)
|
|
102
|
+
except urllib.error.HTTPError as e:
|
|
103
|
+
if e.code == 404:
|
|
104
|
+
rep.critical(f"package '{pkg}' not found on the npm registry")
|
|
105
|
+
else:
|
|
106
|
+
rep.critical(f"npm registry returned HTTP {e.code} (cannot verify)")
|
|
107
|
+
return
|
|
108
|
+
except (urllib.error.URLError, TimeoutError, OSError):
|
|
109
|
+
rep.critical("could not reach the npm registry (offline?) -- cannot verify")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
latest = (data.get("dist-tags") or {}).get("latest")
|
|
113
|
+
versions = data.get("versions") or {}
|
|
114
|
+
meta = versions.get(latest, {}) if latest else {}
|
|
115
|
+
times = data.get("time") or {}
|
|
116
|
+
|
|
117
|
+
if meta.get("deprecated"):
|
|
118
|
+
rep.critical(f"latest version {latest} is DEPRECATED: {str(meta.get('deprecated'))[:80]}")
|
|
119
|
+
created_days = _days_since(times.get("created"))
|
|
120
|
+
last_days = _days_since(times.get(latest)) if latest else None
|
|
121
|
+
maint = data.get("maintainers") or meta.get("maintainers") or []
|
|
122
|
+
|
|
123
|
+
rep.fact(f"latest {latest}, {len(versions)} versions, {len(maint)} maintainer(s)")
|
|
124
|
+
if created_days is not None:
|
|
125
|
+
rep.fact(f"first published {created_days} days ago")
|
|
126
|
+
if created_days < NEW_DAYS:
|
|
127
|
+
rep.warn(f"package is very new ({created_days} days) -- limited track record")
|
|
128
|
+
if last_days is not None and last_days > ABANDONED_DAYS:
|
|
129
|
+
rep.warn(f"no publish in {last_days} days -- possibly abandoned")
|
|
130
|
+
if len(maint) <= 1:
|
|
131
|
+
rep.warn("single maintainer -- bus-factor / takeover risk")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def triage_pip(rep):
|
|
135
|
+
pkg = rep.target
|
|
136
|
+
url = f"https://pypi.org/pypi/{urllib.parse.quote(pkg)}/json"
|
|
137
|
+
try:
|
|
138
|
+
data, _ = _get_json(url)
|
|
139
|
+
except urllib.error.HTTPError as e:
|
|
140
|
+
if e.code == 404:
|
|
141
|
+
rep.critical(f"package '{pkg}' not found on PyPI")
|
|
142
|
+
else:
|
|
143
|
+
rep.critical(f"PyPI returned HTTP {e.code} (cannot verify)")
|
|
144
|
+
return
|
|
145
|
+
except (urllib.error.URLError, TimeoutError, OSError):
|
|
146
|
+
rep.critical("could not reach PyPI (offline?) -- cannot verify")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
info = data.get("info") or {}
|
|
150
|
+
releases = data.get("releases") or {}
|
|
151
|
+
ver = info.get("version")
|
|
152
|
+
files = releases.get(ver) or []
|
|
153
|
+
if any(f.get("yanked") for f in files):
|
|
154
|
+
rep.critical(f"latest version {ver} is YANKED")
|
|
155
|
+
rep.fact(f"latest {ver}, {len(releases)} releases, author: {info.get('author') or 'unknown'}")
|
|
156
|
+
last_iso = files[0].get("upload_time_iso_8601") if files else None
|
|
157
|
+
last_days = _days_since(last_iso)
|
|
158
|
+
if last_days is not None and last_days > ABANDONED_DAYS:
|
|
159
|
+
rep.warn(f"no release in {last_days} days -- possibly abandoned")
|
|
160
|
+
if not info.get("license") and not (info.get("classifiers") or []):
|
|
161
|
+
rep.warn("no declared license -- review terms before use")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _owner_repo(target):
|
|
165
|
+
t = target.strip()
|
|
166
|
+
t = t.replace("https://", "").replace("http://", "")
|
|
167
|
+
t = t.replace("github.com/", "")
|
|
168
|
+
if t.endswith(".git"):
|
|
169
|
+
t = t[:-4]
|
|
170
|
+
parts = [p for p in t.split("/") if p]
|
|
171
|
+
if len(parts) >= 2:
|
|
172
|
+
return f"{parts[0]}/{parts[1]}"
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def triage_repo(rep):
|
|
177
|
+
or_ = _owner_repo(rep.target)
|
|
178
|
+
if not or_:
|
|
179
|
+
rep.critical(f"could not parse owner/repo from '{rep.target}'")
|
|
180
|
+
return
|
|
181
|
+
headers = {}
|
|
182
|
+
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
183
|
+
if token:
|
|
184
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
185
|
+
try:
|
|
186
|
+
data, _ = _get_json(f"https://api.github.com/repos/{or_}", headers=headers)
|
|
187
|
+
except urllib.error.HTTPError as e:
|
|
188
|
+
if e.code == 404:
|
|
189
|
+
rep.critical(f"repo '{or_}' not found (or private)")
|
|
190
|
+
elif e.code in (403, 429):
|
|
191
|
+
rep.critical("GitHub API rate-limited -- cannot verify (set GITHUB_TOKEN and retry)")
|
|
192
|
+
else:
|
|
193
|
+
rep.critical(f"GitHub API returned HTTP {e.code} (cannot verify)")
|
|
194
|
+
return
|
|
195
|
+
except (urllib.error.URLError, TimeoutError, OSError):
|
|
196
|
+
rep.critical("could not reach GitHub (offline?) -- cannot verify")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
rep.fact(f"{or_}: {data.get('stargazers_count', 0)} stars, "
|
|
200
|
+
f"license: {(data.get('license') or {}).get('spdx_id') or 'none'}")
|
|
201
|
+
if data.get("archived"):
|
|
202
|
+
rep.critical(f"repo '{or_}' is ARCHIVED (read-only / unmaintained)")
|
|
203
|
+
if data.get("disabled"):
|
|
204
|
+
rep.critical(f"repo '{or_}' is DISABLED")
|
|
205
|
+
pushed_days = _days_since(data.get("pushed_at"))
|
|
206
|
+
created_days = _days_since(data.get("created_at"))
|
|
207
|
+
if created_days is not None and created_days < NEW_DAYS:
|
|
208
|
+
rep.warn(f"repo is very new ({created_days} days)")
|
|
209
|
+
if pushed_days is not None and pushed_days > ABANDONED_DAYS:
|
|
210
|
+
rep.warn(f"no push in {pushed_days} days -- possibly unmaintained")
|
|
211
|
+
if not (data.get("license") or {}).get("spdx_id"):
|
|
212
|
+
rep.warn("no detected license -- review terms before use")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def triage_docker(rep):
|
|
216
|
+
repo = rep.target
|
|
217
|
+
api_repo = repo if "/" in repo else f"library/{repo}"
|
|
218
|
+
try:
|
|
219
|
+
data, _ = _get_json(f"https://hub.docker.com/v2/repositories/{api_repo}")
|
|
220
|
+
except urllib.error.HTTPError as e:
|
|
221
|
+
if e.code == 404:
|
|
222
|
+
rep.critical(f"image '{repo}' not found on Docker Hub")
|
|
223
|
+
else:
|
|
224
|
+
rep.critical(f"Docker Hub returned HTTP {e.code} (cannot verify)")
|
|
225
|
+
return
|
|
226
|
+
except (urllib.error.URLError, TimeoutError, OSError):
|
|
227
|
+
rep.critical("could not reach Docker Hub (offline?) -- cannot verify")
|
|
228
|
+
return
|
|
229
|
+
rep.fact(f"{api_repo}: {data.get('pull_count', 0)} pulls, official={data.get('is_official', False)}")
|
|
230
|
+
last_days = _days_since(data.get("last_updated"))
|
|
231
|
+
if last_days is not None and last_days > ABANDONED_DAYS:
|
|
232
|
+
rep.warn(f"image not updated in {last_days} days -- stale base / unpatched CVEs likely")
|
|
233
|
+
if not data.get("is_official") and (data.get("pull_count") or 0) < 1000:
|
|
234
|
+
rep.warn("unofficial image with low pull count -- verify the publisher")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def triage_skill(rep):
|
|
238
|
+
target = rep.target
|
|
239
|
+
# Local path -> validate the SKILL.md deterministically.
|
|
240
|
+
candidates = [target, os.path.join(target, "SKILL.md")]
|
|
241
|
+
path = next((p for p in candidates if os.path.isfile(p)), None)
|
|
242
|
+
if path:
|
|
243
|
+
try:
|
|
244
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
245
|
+
text = f.read()
|
|
246
|
+
except OSError as e:
|
|
247
|
+
rep.critical(f"cannot read skill at '{path}': {e}")
|
|
248
|
+
return
|
|
249
|
+
head = text[:400]
|
|
250
|
+
if not head.lstrip().startswith("---"):
|
|
251
|
+
rep.critical("SKILL.md has no YAML frontmatter (--- ... ---)")
|
|
252
|
+
if "name:" not in head:
|
|
253
|
+
rep.warn("frontmatter missing 'name:'")
|
|
254
|
+
if "description:" not in head:
|
|
255
|
+
rep.warn("frontmatter missing 'description:'")
|
|
256
|
+
# crude secret scan
|
|
257
|
+
for marker in ("sk-", "ghp_", "AKIA", "-----BEGIN", "xoxb-", "AIza"):
|
|
258
|
+
if marker in text:
|
|
259
|
+
rep.critical(f"possible secret in skill body (matched '{marker}')")
|
|
260
|
+
rep.fact(f"validated local skill at {path} ({len(text)} bytes)")
|
|
261
|
+
return
|
|
262
|
+
# Otherwise treat a name/owner-repo as a repo triage.
|
|
263
|
+
rep.fact("no local SKILL.md found; treating target as a repo")
|
|
264
|
+
triage_repo(rep)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def main(argv):
|
|
268
|
+
if len(argv) < 1 or argv[0] == "tools":
|
|
269
|
+
print("kit triage -- available checks:")
|
|
270
|
+
print(" npm <pkg> npm registry: existence, deprecation, age, maintainers")
|
|
271
|
+
print(" pip <pkg> PyPI: existence, yanked, age, license")
|
|
272
|
+
print(" repo <owner/repo|url> GitHub: archived, maintenance, license, age")
|
|
273
|
+
print(" docker <image> Docker Hub: existence, freshness, publisher")
|
|
274
|
+
print(" skill <path|name> validate a local SKILL.md, else repo-check")
|
|
275
|
+
return 0
|
|
276
|
+
ttype = argv[0]
|
|
277
|
+
target = argv[1] if len(argv) > 1 else ""
|
|
278
|
+
if ttype == "all" or not target:
|
|
279
|
+
print(f"Usage: triage.py <npm|pip|repo|docker|skill> <target>")
|
|
280
|
+
return 0
|
|
281
|
+
rep = Report(ttype, target)
|
|
282
|
+
dispatch = {
|
|
283
|
+
"npm": triage_npm,
|
|
284
|
+
"pip": triage_pip,
|
|
285
|
+
"repo": triage_repo,
|
|
286
|
+
"docker": triage_docker,
|
|
287
|
+
"skill": triage_skill,
|
|
288
|
+
}
|
|
289
|
+
fn = dispatch.get(ttype)
|
|
290
|
+
if not fn:
|
|
291
|
+
rep.critical(f"unknown triage type '{ttype}'")
|
|
292
|
+
else:
|
|
293
|
+
fn(rep)
|
|
294
|
+
rep.emit()
|
|
295
|
+
return 0
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
import urllib.parse # noqa: E402 (kept local to module load is fine)
|
|
300
|
+
sys.exit(main(sys.argv[1:]))
|