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.
@@ -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":"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"}
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 { resolve } from "node:path";
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 TRIAGE_SCRIPT = resolve(process.env.HOME || "~", ".claude/skills/triage/scripts/triage.py");
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
- * Check if the triage script exists
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 false;
46
+ return installBundledTriageSkill();
21
47
  }
22
48
  }
23
49
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"triage.js","sourceRoot":"","sources":["../src/triage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"triage.js","sourceRoot":"","sources":["../src/triage.ts"],"names":[],"mappings":"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"}
@@ -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;
@@ -1,6 +1,9 @@
1
1
  import { readFile, writeFile, mkdir } from "node:fs/promises";
2
- import { join } from "node:path";
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}npm install -g ${PACKAGE_NAME}${reset}`);
122
+ `${dim}run ${reset}${cyan}kit upgrade --self${reset}${dim} (triages before installing)${reset}`);
90
123
  }
91
124
  //# sourceMappingURL=update-check.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"update-check.js","sourceRoot":"","sources":["../src/update-check.ts"],"names":[],"mappings":"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"}
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.8.0",
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:]))