@interlok/cli 1.3.0 → 1.3.1
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/README.md +1 -1
- package/dist/index.js +14 -3
- package/dist/inventory.d.ts +14 -0
- package/dist/inventory.js +33 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npx @interlok/cli scan --ci --min-coverage 80 # exit 5 below 80% of high-risk wr
|
|
|
11
11
|
npx @interlok/cli scan --publish # signed public report + README badge (needs a key)
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Coverage counts only high-consequence write paths (money, access, deletion, code execution, deploys, messages to people) and how many have a guard. Tools whose names can't be classified count as high-risk until someone classifies them. `src/classify.ts` is the same file the hosted app uses; a test fails the build if they differ.
|
|
14
|
+
Coverage counts only high-consequence write paths (money, access, deletion, code execution, deploys, messages to people) and how many have a guard. Tools whose names can't be classified count as high-risk until someone classifies them. A config file names servers but usually not their tools: without `--enumerate` (or an inventory that lists tools) the scan says which servers it could not check instead of calling them safe, and `--min-coverage` exits 5 because coverage cannot be measured. `src/classify.ts` is the same file the hosted app uses; a test fails the build if they differ.
|
|
15
15
|
|
|
16
16
|
## shadow: what your agent would have done, on every pull request
|
|
17
17
|
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { scan } from "./scan.js";
|
|
|
5
5
|
import { verifyFile } from "./verify.js";
|
|
6
6
|
import { verifyProofBundle } from "./proof-verify.js";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
|
-
import { coverageOf, enumerate, findInventory } from "./inventory.js";
|
|
8
|
+
import { coverageOf, enumerate, findInventory, scanVerdict, unlistedServers } from "./inventory.js";
|
|
9
9
|
import { coverageLine } from "./classify.js";
|
|
10
10
|
import { gateCommand } from "./gate.js";
|
|
11
11
|
import { proxyCommand } from "./proxy.js";
|
|
@@ -40,6 +40,10 @@ async function main() {
|
|
|
40
40
|
if (has("--enumerate"))
|
|
41
41
|
inventory = await enumerate(inventory);
|
|
42
42
|
const coverage = coverageOf(inventory);
|
|
43
|
+
const verdict = scanVerdict(coverage, inventory, coverageLine(coverage, "guarded"), {
|
|
44
|
+
filesFound: files.length > 0,
|
|
45
|
+
enumerated: has("--enumerate"),
|
|
46
|
+
});
|
|
43
47
|
if (value("--format") === "cef" || has("--format=cef")) {
|
|
44
48
|
findings.forEach((f) => console.log(`CEF:0|Interlok|CLI|1|${f.code}|${f.message}|${f.severity === "critical" ? 10 : 6}|file=${f.file}`));
|
|
45
49
|
}
|
|
@@ -47,12 +51,14 @@ async function main() {
|
|
|
47
51
|
console.log(JSON.stringify({
|
|
48
52
|
target,
|
|
49
53
|
inventory_files: files,
|
|
54
|
+
servers: inventory.servers.length,
|
|
55
|
+
servers_without_tools: unlistedServers(inventory),
|
|
50
56
|
coverage,
|
|
51
57
|
findings,
|
|
52
58
|
critical: findings.filter((f) => f.severity === "critical").length,
|
|
53
59
|
}, null, 2));
|
|
54
60
|
}
|
|
55
|
-
console.error(`coverage ${
|
|
61
|
+
console.error(`coverage ${verdict.text}`);
|
|
56
62
|
if (has("--publish")) {
|
|
57
63
|
if (!api || !key)
|
|
58
64
|
throw new Error("--publish needs INTERLOK_URL and INTERLOK_API_KEY");
|
|
@@ -79,7 +85,12 @@ async function main() {
|
|
|
79
85
|
const min = value("--min-coverage");
|
|
80
86
|
if (min !== undefined) {
|
|
81
87
|
const pct = coverage.rungs.guarded?.pct ?? null;
|
|
82
|
-
|
|
88
|
+
// A CI gate must not pass on servers it could not see.
|
|
89
|
+
if (!verdict.measurable && inventory.servers.length) {
|
|
90
|
+
console.error(`coverage cannot be measured: ${verdict.text}`);
|
|
91
|
+
process.exitCode = 5;
|
|
92
|
+
}
|
|
93
|
+
else if (pct !== null && pct < Number(min)) {
|
|
83
94
|
console.error(`coverage ${pct}% is below --min-coverage ${min}`);
|
|
84
95
|
process.exitCode = 5;
|
|
85
96
|
}
|
package/dist/inventory.d.ts
CHANGED
|
@@ -34,6 +34,20 @@ export declare function findInventory(target: string): Promise<{
|
|
|
34
34
|
}>;
|
|
35
35
|
/** The same normalisation the console uses: an inventory passes through; a client config is mapped. */
|
|
36
36
|
export declare function toInventory(raw: any): Inventory;
|
|
37
|
+
/** Servers whose tools are not listed in the inventory, so coverage cannot see them. */
|
|
38
|
+
export declare function unlistedServers(inv: Inventory): string[];
|
|
39
|
+
/**
|
|
40
|
+
* The one-line verdict printed after a scan. "No high-risk write paths" is only said when every
|
|
41
|
+
* server's tools were actually seen; a config that names servers without listing their tools gets
|
|
42
|
+
* told to enumerate instead of being told it is safe.
|
|
43
|
+
*/
|
|
44
|
+
export declare function scanVerdict(coverage: Coverage, inv: Inventory, line: string, opts: {
|
|
45
|
+
filesFound: boolean;
|
|
46
|
+
enumerated: boolean;
|
|
47
|
+
}): {
|
|
48
|
+
text: string;
|
|
49
|
+
measurable: boolean;
|
|
50
|
+
};
|
|
37
51
|
export declare function coverageOf(inv: Inventory): Coverage;
|
|
38
52
|
/** Start each server (or call it over HTTP) and list its tools. Opt-in: this runs the configured commands. */
|
|
39
53
|
export declare function enumerate(inv: Inventory, timeoutMs?: number): Promise<Inventory>;
|
package/dist/inventory.js
CHANGED
|
@@ -53,6 +53,39 @@ export function toInventory(raw) {
|
|
|
53
53
|
})),
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
/** Servers whose tools are not listed in the inventory, so coverage cannot see them. */
|
|
57
|
+
export function unlistedServers(inv) {
|
|
58
|
+
return inv.servers.filter((s) => !s.tools?.length).map((s) => s.name);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The one-line verdict printed after a scan. "No high-risk write paths" is only said when every
|
|
62
|
+
* server's tools were actually seen; a config that names servers without listing their tools gets
|
|
63
|
+
* told to enumerate instead of being told it is safe.
|
|
64
|
+
*/
|
|
65
|
+
export function scanVerdict(coverage, inv, line, opts) {
|
|
66
|
+
if (!opts.filesFound)
|
|
67
|
+
return {
|
|
68
|
+
text: "no MCP config or inventory found; pass a file, or --enumerate to list tools",
|
|
69
|
+
measurable: false,
|
|
70
|
+
};
|
|
71
|
+
const unlisted = unlistedServers(inv);
|
|
72
|
+
const n = inv.servers.length;
|
|
73
|
+
if (!unlisted.length)
|
|
74
|
+
return { text: line, measurable: true };
|
|
75
|
+
const names = unlisted.slice(0, 5).join(", ") + (unlisted.length > 5 ? ", …" : "");
|
|
76
|
+
const hint = opts.enumerated
|
|
77
|
+
? "they did not answer when started; check their commands and credentials"
|
|
78
|
+
: "run with --enumerate to start them and check their tools";
|
|
79
|
+
if (unlisted.length === n)
|
|
80
|
+
return {
|
|
81
|
+
text: `${n} server${n === 1 ? "" : "s"} found (${names}), tools not listed: ${hint}`,
|
|
82
|
+
measurable: false,
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
text: `${line} · ${unlisted.length} of ${n} servers not checked (${names}): ${hint}`,
|
|
86
|
+
measurable: coverage.high_risk_write_paths > 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
56
89
|
export function coverageOf(inv) {
|
|
57
90
|
const items = [];
|
|
58
91
|
for (const s of inv.servers)
|
package/package.json
CHANGED