@interlok/cli 0.0.0-stage → 1.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/LICENSE +5 -0
- package/README.md +158 -2
- package/dist/approvals-mcp.d.ts +83 -0
- package/dist/approvals-mcp.js +185 -0
- package/dist/canonical.d.ts +1 -0
- package/dist/canonical.js +20 -0
- package/dist/classify.d.ts +107 -0
- package/dist/classify.js +524 -0
- package/dist/gate.d.ts +19 -0
- package/dist/gate.js +338 -0
- package/dist/hook.d.ts +108 -0
- package/dist/hook.js +578 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +230 -0
- package/dist/inventory.d.ts +39 -0
- package/dist/inventory.js +177 -0
- package/dist/packs.d.ts +105 -0
- package/dist/packs.js +186 -0
- package/dist/proof-verify.d.ts +108 -0
- package/dist/proof-verify.js +513 -0
- package/dist/proxy.d.ts +69 -0
- package/dist/proxy.js +361 -0
- package/dist/scan.d.ts +7 -0
- package/dist/scan.js +41 -0
- package/dist/verify.d.ts +34 -0
- package/dist/verify.js +123 -0
- package/package.json +31 -4
package/dist/packs.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The change-control-v1 (with its model-change rules) and access-control-v1 packs: deterministic predicates over a normalized
|
|
3
|
+
* request. A request is held when any rule with `hold: true` fires or its points reach the
|
|
4
|
+
* threshold (50). Nothing here calls a model or the network, and the same input always yields the
|
|
5
|
+
* same rules, so a CI run, the console and a test agree.
|
|
6
|
+
*/
|
|
7
|
+
export const HOLD_THRESHOLD = 50;
|
|
8
|
+
/**
|
|
9
|
+
* A model promotion: what a production AI system will run after this change. Swapping a provider,
|
|
10
|
+
* a model, its weights, its quantization or its prompt template changes behaviour as surely as a
|
|
11
|
+
* code deploy, and usually without a diff anyone reads.
|
|
12
|
+
*/
|
|
13
|
+
export const MODEL_CHANGE_KINDS = [
|
|
14
|
+
"model",
|
|
15
|
+
"provider",
|
|
16
|
+
"weights",
|
|
17
|
+
"fine_tune",
|
|
18
|
+
"quantization",
|
|
19
|
+
"prompt_template",
|
|
20
|
+
"decoding",
|
|
21
|
+
"alias",
|
|
22
|
+
];
|
|
23
|
+
const IAM = /(^|[._])(aws_iam_|aws_kms_|google_[a-z_]*_iam_|azurerm_role_assignment|azurerm_key_vault|aws_organizations)/i;
|
|
24
|
+
const DATA = /(^|[._])(aws_db_|aws_rds_|aws_dynamodb_|aws_s3_bucket|google_sql_|google_storage_bucket|azurerm_postgresql|azurerm_mssql|azurerm_storage|aws_elasticache|mongodbatlas_)/i;
|
|
25
|
+
const MIGRATION_DEFAULT = [
|
|
26
|
+
"migrations/",
|
|
27
|
+
"db/migrate/",
|
|
28
|
+
"prisma/migrations/",
|
|
29
|
+
"drizzle/migrations/",
|
|
30
|
+
"alembic/",
|
|
31
|
+
];
|
|
32
|
+
export function evaluateChange(c, overrides = {}) {
|
|
33
|
+
const fired = [];
|
|
34
|
+
const add = (id, reason, points, hold) => fired.push({ id, reason, points, hold });
|
|
35
|
+
const plan = c.plan;
|
|
36
|
+
const addresses = plan?.addresses ?? [];
|
|
37
|
+
const prodTags = (c.production_tags?.length ? c.production_tags : ["production", "prod", "live"]).map((t) => t.toLowerCase());
|
|
38
|
+
const isProd = !!c.environment && prodTags.includes(c.environment.toLowerCase());
|
|
39
|
+
if (plan && (plan.destroy > 0 || plan.replace > 0))
|
|
40
|
+
add("CC-DESTROY", `Plan destroys ${plan.destroy} and replaces ${plan.replace} resource(s)`, 40, true);
|
|
41
|
+
if (addresses.some((a) => IAM.test(a)))
|
|
42
|
+
add("CC-IAM", "Plan touches IAM, roles, policies or KMS", 40, true);
|
|
43
|
+
if (plan?.open_cidrs?.length)
|
|
44
|
+
add("CC-NET-OPEN", `Opens ${plan.open_cidrs.join(", ")} to the world`, 40, true);
|
|
45
|
+
if (c.author_kinds.some((k) => k === "agent" || k === "bot")) {
|
|
46
|
+
const pts = overrides.agent_author_points ?? null;
|
|
47
|
+
add("CC-AGENT-AUTHOR", "Change authored by a non-human actor", pts ?? 30, pts === null);
|
|
48
|
+
}
|
|
49
|
+
if (c.pr_approved_by_human === false)
|
|
50
|
+
add("CC-NO-REVIEW", "Merged without an approving human review", 30, true);
|
|
51
|
+
if (c.freeze_active)
|
|
52
|
+
add("CC-FREEZE", "A change-freeze window is active", 30, true);
|
|
53
|
+
if (addresses.some((a) => DATA.test(a)))
|
|
54
|
+
add("CC-DATA-STORE", "Changes a database, bucket or storage resource", 30, false);
|
|
55
|
+
const globs = c.migration_globs?.length ? c.migration_globs : MIGRATION_DEFAULT;
|
|
56
|
+
if ((c.changed_paths ?? []).some((p) => globs.some((g) => p.includes(g))))
|
|
57
|
+
add("CC-MIGRATION", "Changed paths include a database migration", 30, false);
|
|
58
|
+
if (c.in_change_window === false)
|
|
59
|
+
add("CC-OUT-OF-WINDOW", "Outside the configured change window", 30, false);
|
|
60
|
+
if (isProd)
|
|
61
|
+
add("CC-PROD-ENV", `Target environment ${c.environment} is production`, 20, false);
|
|
62
|
+
if (c.action === "model.promote" || c.model)
|
|
63
|
+
modelRules(c.model ?? null, add);
|
|
64
|
+
const resources = plan ? plan.add + plan.change + plan.destroy + plan.replace : 0;
|
|
65
|
+
if ((c.files_changed ?? 0) > 50 || resources > 25)
|
|
66
|
+
add("CC-SIZE", "Large change: more than 50 files or 25 resources", 15, false);
|
|
67
|
+
const points = fired.reduce((n, f) => n + f.points, 0);
|
|
68
|
+
return { hold: fired.some((f) => f.hold) || points >= HOLD_THRESHOLD, points, fired };
|
|
69
|
+
}
|
|
70
|
+
/** Floating names: the target can change under you without another promotion. */
|
|
71
|
+
const FLOATING = /(^|[-_:@/.])(latest|preview|default|current|stable|exp(erimental)?)$/i;
|
|
72
|
+
function modelRules(m, add) {
|
|
73
|
+
if (!m) {
|
|
74
|
+
add("MC-UNDESCRIBED", "Model promotion without a description of what changes", 40, true);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const ch = new Set(m.changes ?? []);
|
|
78
|
+
const lc = (s) => (s ? s.toLowerCase() : null);
|
|
79
|
+
if (lc(m.provider_from) && lc(m.provider_to) && lc(m.provider_from) !== lc(m.provider_to))
|
|
80
|
+
add("MC-PROVIDER-SWAP", `Provider changes from ${m.provider_from} to ${m.provider_to}`, 40, true);
|
|
81
|
+
else if (ch.has("provider"))
|
|
82
|
+
add("MC-PROVIDER-SWAP", "Provider changes", 40, true);
|
|
83
|
+
if ((m.digest_from && m.digest_to && m.digest_from !== m.digest_to) ||
|
|
84
|
+
ch.has("weights") ||
|
|
85
|
+
ch.has("fine_tune"))
|
|
86
|
+
add("MC-WEIGHTS", "Model weights change (new checkpoint or fine-tune)", 30, true);
|
|
87
|
+
if (ch.has("alias") || FLOATING.test(m.model_to))
|
|
88
|
+
add("MC-FLOATING", `Target ${m.model_to} is a floating alias, not a pinned version`, 30, true);
|
|
89
|
+
if (m.eval && (m.eval.passed === false || (m.eval.regressions ?? 0) > 0))
|
|
90
|
+
add("MC-EVAL-FAIL", `Evaluation ${m.eval.suite ?? ""} ${m.eval.passed === false ? "failed" : `shows ${m.eval.regressions} regression(s)`}`.replace(/\s+/g, " "), 40, true);
|
|
91
|
+
else if (!m.eval || m.eval.passed === null || m.eval.passed === undefined)
|
|
92
|
+
add("MC-NO-EVAL", "No evaluation result attached to the promotion", 30, false);
|
|
93
|
+
if ((m.classification ?? []).length)
|
|
94
|
+
add("MC-GOVERNED-DATA", `Model serves governed data: ${(m.classification ?? []).join(", ")}`, 30, true);
|
|
95
|
+
if (ch.has("model") && lc(m.model_from) !== lc(m.model_to))
|
|
96
|
+
add("MC-MODEL-SWAP", `Model changes from ${m.model_from ?? "unstated"} to ${m.model_to}`, 20, false);
|
|
97
|
+
if (ch.has("quantization"))
|
|
98
|
+
add("MC-QUANT", "Quantization changes", 20, false);
|
|
99
|
+
if (ch.has("prompt_template"))
|
|
100
|
+
add("MC-PROMPT", "System prompt or template changes", 20, false);
|
|
101
|
+
if (ch.has("decoding"))
|
|
102
|
+
add("MC-DECODING", "Decoding parameters change", 10, false);
|
|
103
|
+
}
|
|
104
|
+
export const MAX_DURATION_S = {
|
|
105
|
+
high: 3600,
|
|
106
|
+
medium: 4 * 3600,
|
|
107
|
+
low: 8 * 3600,
|
|
108
|
+
};
|
|
109
|
+
const ADMIN = /(admin|owner|root|superuser)/i;
|
|
110
|
+
export function evaluateAccess(c) {
|
|
111
|
+
const fired = [];
|
|
112
|
+
const add = (id, reason, points, hold) => fired.push({ id, reason, points, hold });
|
|
113
|
+
let deny = false;
|
|
114
|
+
const cap_s = MAX_DURATION_S[c.sensitivity];
|
|
115
|
+
if (c.requester_kind !== "human" && c.requester_has_owner === false) {
|
|
116
|
+
add("AC-ORPHAN-NHI", "Non-human requester has no human owner", 0, true);
|
|
117
|
+
deny = true;
|
|
118
|
+
}
|
|
119
|
+
if (c.requester_kind !== "human")
|
|
120
|
+
add("AC-NHI-ELEVATE", "Requester is an agent or service account", 30, true);
|
|
121
|
+
if (ADMIN.test(c.role) && c.is_production)
|
|
122
|
+
add("AC-PROD-ADMIN", `Admin-level role ${c.role} on a production resource`, 40, true);
|
|
123
|
+
if (c.sensitivity === "high")
|
|
124
|
+
add("AC-SENSITIVE", "Resource holds sensitive data", 30, true);
|
|
125
|
+
if (c.duration_requested_s > cap_s)
|
|
126
|
+
add("AC-LONG", `Requested ${Math.round(c.duration_requested_s / 60)} min exceeds the ${Math.round(cap_s / 60)} min cap; capped`, 20, true);
|
|
127
|
+
const [open, close] = c.business_hours ?? [8, 18];
|
|
128
|
+
if (c.hour_local !== null &&
|
|
129
|
+
c.hour_local !== undefined &&
|
|
130
|
+
(c.hour_local < open || c.hour_local >= close))
|
|
131
|
+
add("AC-OFF-HOURS", "Requested outside business hours", 30, false);
|
|
132
|
+
if (!c.ticket_ref)
|
|
133
|
+
add("AC-NO-TICKET", "No ticket reference", 20, false);
|
|
134
|
+
if ((c.recent_elevations ?? 0) > 3)
|
|
135
|
+
add("AC-REPEAT", "More than 3 elevations on this resource in 7 days: standing-access candidate", 20, false);
|
|
136
|
+
const points = fired.reduce((n, f) => n + f.points, 0);
|
|
137
|
+
return {
|
|
138
|
+
hold: deny || fired.some((f) => f.hold) || points >= HOLD_THRESHOLD,
|
|
139
|
+
deny,
|
|
140
|
+
points,
|
|
141
|
+
fired,
|
|
142
|
+
cap_s,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** `terraform show -json` → the summary the CLI sends. Runs in CI; the full plan never leaves it. */
|
|
146
|
+
export function summarizePlan(plan) {
|
|
147
|
+
const rc = Array.isArray(plan?.resource_changes) ? plan.resource_changes : [];
|
|
148
|
+
const s = {
|
|
149
|
+
add: 0,
|
|
150
|
+
change: 0,
|
|
151
|
+
destroy: 0,
|
|
152
|
+
replace: 0,
|
|
153
|
+
resource_types: [],
|
|
154
|
+
addresses: [],
|
|
155
|
+
open_cidrs: [],
|
|
156
|
+
};
|
|
157
|
+
const types = new Set();
|
|
158
|
+
for (const r of rc) {
|
|
159
|
+
const actions = r?.change?.actions ?? [];
|
|
160
|
+
if (actions.includes("create") && actions.includes("delete"))
|
|
161
|
+
s.replace++;
|
|
162
|
+
else if (actions.includes("delete"))
|
|
163
|
+
s.destroy++;
|
|
164
|
+
else if (actions.includes("create"))
|
|
165
|
+
s.add++;
|
|
166
|
+
else if (actions.includes("update"))
|
|
167
|
+
s.change++;
|
|
168
|
+
else
|
|
169
|
+
continue;
|
|
170
|
+
if (r.type)
|
|
171
|
+
types.add(r.type);
|
|
172
|
+
if (r.address)
|
|
173
|
+
s.addresses.push(r.address);
|
|
174
|
+
const after = r?.change?.after;
|
|
175
|
+
const text = after ? JSON.stringify(after) : "";
|
|
176
|
+
if (/aws_security_group|google_compute_firewall|azurerm_network_security/.test(String(r.type ?? "")) &&
|
|
177
|
+
/"0\.0\.0\.0\/0"|"::\/0"/.test(text))
|
|
178
|
+
for (const c of ["0.0.0.0/0", "::/0"])
|
|
179
|
+
if (text.includes(`"${c}"`) && !s.open_cidrs.includes(c))
|
|
180
|
+
s.open_cidrs.push(c);
|
|
181
|
+
}
|
|
182
|
+
s.resource_types = [...types].sort();
|
|
183
|
+
return s;
|
|
184
|
+
}
|
|
185
|
+
/** CLI exit codes for the gate step. */
|
|
186
|
+
export const EXIT = { ok: 0, rejected: 10, expired: 11, unreachable: 12, config: 13 };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export declare const PROOF_FORMAT = "interlok.proof/1";
|
|
2
|
+
export declare const DSSE_PAYLOAD_TYPE = "application/vnd.interlok.proof-manifest+json";
|
|
3
|
+
export declare const STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
4
|
+
export declare const PREDICATE_TYPE = "https://interlok.io/attestation/determination/v1";
|
|
5
|
+
export type CheckStatus = "pass" | "fail" | "skipped";
|
|
6
|
+
export interface RecordProblem {
|
|
7
|
+
seq: number | null;
|
|
8
|
+
problem: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CheckResult {
|
|
11
|
+
id: number;
|
|
12
|
+
name: string;
|
|
13
|
+
status: CheckStatus;
|
|
14
|
+
detail: string;
|
|
15
|
+
records: RecordProblem[];
|
|
16
|
+
}
|
|
17
|
+
export interface VerifyResult {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
format: string | null;
|
|
20
|
+
bundle_id: string | null;
|
|
21
|
+
org: {
|
|
22
|
+
id: string;
|
|
23
|
+
name?: string | null;
|
|
24
|
+
} | null;
|
|
25
|
+
record_count: number;
|
|
26
|
+
checks: CheckResult[];
|
|
27
|
+
}
|
|
28
|
+
export interface VerifyOptions {
|
|
29
|
+
/** Anchors fetched from the public anchor feed. Absent: check 6 is "not checked". */
|
|
30
|
+
publicAnchors?: Array<{
|
|
31
|
+
merkle_root: string;
|
|
32
|
+
from_seq?: number;
|
|
33
|
+
to_seq?: number;
|
|
34
|
+
}> | null;
|
|
35
|
+
/** Key ids (kid) the customer publishes at /.well-known/interlok-keys.json. Absent: check 7 skipped. */
|
|
36
|
+
publishedKids?: string[] | null;
|
|
37
|
+
/** Grace after a key's valid_to (rotation overlap), in ms. */
|
|
38
|
+
keyGraceMs?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ChainRecord {
|
|
41
|
+
seq: number;
|
|
42
|
+
prev_hash: string;
|
|
43
|
+
payload_hash: string;
|
|
44
|
+
entry_hash: string;
|
|
45
|
+
signature: string;
|
|
46
|
+
signer_kid: string;
|
|
47
|
+
/** Present when a v1 record's content is revealed; absent for hash-only records. */
|
|
48
|
+
payload?: Record<string, unknown>;
|
|
49
|
+
/** Ledger record v2: per-field reveals or commitments, instead of payload. */
|
|
50
|
+
record_version?: number;
|
|
51
|
+
fields?: Record<string, FieldReveal>;
|
|
52
|
+
}
|
|
53
|
+
export interface KeyEntry {
|
|
54
|
+
jwk: JsonWebKey;
|
|
55
|
+
valid_from: string | null;
|
|
56
|
+
valid_to: string | null;
|
|
57
|
+
custody?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface AnchorEntry {
|
|
60
|
+
id: string;
|
|
61
|
+
from_seq: number;
|
|
62
|
+
to_seq: number;
|
|
63
|
+
merkle_root: string;
|
|
64
|
+
head_hash: string;
|
|
65
|
+
anchor: Record<string, unknown> & {
|
|
66
|
+
anchor_hash: string;
|
|
67
|
+
signature: string;
|
|
68
|
+
signer_kid: string;
|
|
69
|
+
merkle_root: string;
|
|
70
|
+
};
|
|
71
|
+
published_to?: string | null;
|
|
72
|
+
publish_ref?: string | null;
|
|
73
|
+
}
|
|
74
|
+
export interface ProofStep {
|
|
75
|
+
hash: string;
|
|
76
|
+
position: "left" | "right";
|
|
77
|
+
}
|
|
78
|
+
/** Canonical JSON: sorted keys, no whitespace, undefined dropped. Same as the ledger's. */
|
|
79
|
+
export declare function canonicalize(value: unknown): string;
|
|
80
|
+
export declare function sha256Hex(data: string | Uint8Array): Promise<string>;
|
|
81
|
+
export declare function fromB64url(s: string): Uint8Array;
|
|
82
|
+
export declare function toB64(u: Uint8Array): string;
|
|
83
|
+
export declare function fromB64(s: string): Uint8Array;
|
|
84
|
+
export declare function verifySig(jwk: JsonWebKey, sigB64url: string, data: string | Uint8Array): Promise<boolean>;
|
|
85
|
+
/** DSSE pre-authentication encoding: "DSSEv1 <len(type)> <type> <len(body)> <body>". */
|
|
86
|
+
export declare function pae(payloadType: string, body: Uint8Array): Uint8Array;
|
|
87
|
+
export declare function verifyMerkle(leaf: string, proof: ProofStep[], root: string): Promise<boolean>;
|
|
88
|
+
export declare function flattenPayload(value: unknown, path?: string): Array<{
|
|
89
|
+
path: string;
|
|
90
|
+
value: unknown;
|
|
91
|
+
}>;
|
|
92
|
+
export declare const leafHash: (salt: string, path: string, value: unknown) => Promise<string>;
|
|
93
|
+
export declare function merkleRootHex(leaves: string[]): Promise<string>;
|
|
94
|
+
export type FieldReveal = {
|
|
95
|
+
v: unknown;
|
|
96
|
+
s: string;
|
|
97
|
+
} | {
|
|
98
|
+
c: string;
|
|
99
|
+
};
|
|
100
|
+
/** The root over a record's fields, revealed or committed. */
|
|
101
|
+
export declare function rootFromFields(fields: Record<string, FieldReveal>): Promise<string>;
|
|
102
|
+
/** The root for a full payload and its salts (one salt per leaf path). */
|
|
103
|
+
export declare function payloadRootV2(payload: unknown, salts: Record<string, string>): Promise<string>;
|
|
104
|
+
/** What an auditor sees: revealed values in place, hidden ones as "[redacted]". */
|
|
105
|
+
export declare function viewFromFields(fields: Record<string, FieldReveal>): Record<string, unknown>;
|
|
106
|
+
/** In-scope record → in-toto Statement v1. Used by the builder and re-derived by the verifier. */
|
|
107
|
+
export declare function statementFor(org: string, r: ChainRecord): Record<string, unknown>;
|
|
108
|
+
export declare function verifyProofBundle(zip: Uint8Array, opts?: VerifyOptions): Promise<VerifyResult>;
|