@zanii/blackbox 0.2.0 → 0.4.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/README.md +26 -1
- package/dist/a2a/index.d.ts +77 -0
- package/dist/a2a/index.js +305 -0
- package/dist/agents/index.d.ts +8 -0
- package/dist/analysis/accuracy.d.ts +24 -0
- package/dist/analysis/accuracy.js +45 -0
- package/dist/analysis/credential.d.ts +101 -0
- package/dist/analysis/credential.js +142 -0
- package/dist/analysis/faults.js +115 -0
- package/dist/analysis/grounding.d.ts +122 -0
- package/dist/analysis/grounding.js +445 -0
- package/dist/analysis/hallucination.d.ts +32 -0
- package/dist/analysis/hallucination.js +357 -0
- package/dist/analysis/index.d.ts +23 -0
- package/dist/analysis/index.js +93 -0
- package/dist/analysis/memory.d.ts +8 -0
- package/dist/analysis/memory.js +35 -8
- package/dist/analysis/reference.d.ts +49 -0
- package/dist/analysis/reference.js +164 -0
- package/dist/analysis/taxonomy.d.ts +18 -0
- package/dist/analysis/taxonomy.js +66 -0
- package/dist/approvals/index.d.ts +23 -0
- package/dist/approvals/index.js +48 -0
- package/dist/archive/index.d.ts +39 -0
- package/dist/archive/index.js +96 -0
- package/dist/archive/parquet.d.ts +2 -0
- package/dist/archive/parquet.js +185 -0
- package/dist/badge/index.d.ts +16 -0
- package/dist/badge/index.js +48 -0
- package/dist/bom/index.d.ts +14 -0
- package/dist/bom/index.js +152 -0
- package/dist/cli.js +114 -10
- package/dist/compliance/art12.d.ts +35 -0
- package/dist/compliance/art12.js +190 -0
- package/dist/compliance/index.d.ts +36 -2
- package/dist/compliance/index.js +78 -11
- package/dist/compliance/zanii.d.ts +29 -0
- package/dist/compliance/zanii.js +84 -0
- package/dist/constitution/index.d.ts +57 -0
- package/dist/constitution/index.js +131 -0
- package/dist/cv/index.d.ts +39 -0
- package/dist/cv/index.js +108 -0
- package/dist/disclosure/index.d.ts +31 -0
- package/dist/disclosure/index.js +113 -0
- package/dist/encryption/index.d.ts +9 -0
- package/dist/encryption/index.js +31 -0
- package/dist/evidence/index.d.ts +60 -0
- package/dist/evidence/index.js +151 -0
- package/dist/federation/index.d.ts +35 -0
- package/dist/federation/index.js +102 -0
- package/dist/finance/index.d.ts +126 -0
- package/dist/finance/index.js +320 -0
- package/dist/fleet/index.js +9 -0
- package/dist/gov/index.d.ts +108 -0
- package/dist/gov/index.js +225 -0
- package/dist/health/index.d.ts +120 -0
- package/dist/health/index.js +233 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.js +34 -5
- package/dist/memory/index.d.ts +36 -0
- package/dist/memory/index.js +85 -0
- package/dist/occurrence/index.d.ts +11 -0
- package/dist/occurrence/index.js +18 -0
- package/dist/ocsf/index.d.ts +1 -1
- package/dist/ocsf/index.js +36 -3
- package/dist/otlp/index.d.ts +8 -1
- package/dist/otlp/index.js +258 -1
- package/dist/packs/index.js +44 -4
- package/dist/policy/delta.js +7 -1
- package/dist/policy/index.d.ts +40 -6
- package/dist/policy/index.js +186 -8
- package/dist/policy/zanii.d.ts +31 -0
- package/dist/policy/zanii.js +87 -0
- package/dist/pq/index.d.ts +23 -0
- package/dist/pq/index.js +104 -0
- package/dist/search/index.d.ts +23 -0
- package/dist/search/index.js +69 -0
- package/dist/session/index.d.ts +107 -1
- package/dist/session/index.js +189 -11
- package/dist/sla/index.d.ts +61 -0
- package/dist/sla/index.js +197 -0
- package/dist/succession/index.d.ts +50 -0
- package/dist/succession/index.js +123 -0
- package/dist/timestamp/index.d.ts +24 -0
- package/dist/timestamp/index.js +274 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.js +46 -0
- package/dist/transparency/index.d.ts +188 -0
- package/dist/transparency/index.js +712 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walls/index.d.ts +31 -0
- package/dist/walls/index.js +119 -0
- package/package.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Fact } from "./grounding.ts";
|
|
2
|
+
export interface ReferenceFact {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Terms that name what the fact is about (`SOFA-221`, `refund`, `استرداد`): one of them must be in
|
|
5
|
+
* the answer's sentence for the fact to apply. */
|
|
6
|
+
about: string[];
|
|
7
|
+
kind: "money" | "percent" | "quantity" | "date" | "text";
|
|
8
|
+
/** Canonical (`1250.5`, `20261015`); absent for `text`. */
|
|
9
|
+
value?: string;
|
|
10
|
+
currency?: string;
|
|
11
|
+
unit?: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
source?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ReferencePack {
|
|
16
|
+
id: string;
|
|
17
|
+
version: string;
|
|
18
|
+
tenant?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
facts: ReferenceFact[];
|
|
21
|
+
/** `sha256:` of the pack's RFC 8785 JSON as loaded. */
|
|
22
|
+
hash: string;
|
|
23
|
+
}
|
|
24
|
+
/** spec/findings.md §10: the packs in a reference file, checked; throws with the first problem. */
|
|
25
|
+
export declare function loadReferencePacks(bytes: Uint8Array): ReferencePack[];
|
|
26
|
+
/** The packs a session's tenant sees: its own, and those for everyone. */
|
|
27
|
+
export declare function packsFor(packs: readonly ReferencePack[], tenant: string | undefined): ReferencePack[];
|
|
28
|
+
export type ReferenceVerdict = {
|
|
29
|
+
status: "grounded";
|
|
30
|
+
pack: ReferencePack;
|
|
31
|
+
fact: ReferenceFact;
|
|
32
|
+
} | {
|
|
33
|
+
status: "contradicted";
|
|
34
|
+
pack: ReferencePack;
|
|
35
|
+
fact: ReferenceFact;
|
|
36
|
+
} | null;
|
|
37
|
+
/**
|
|
38
|
+
* §10: what the packs say about one fact of an answer. The pack facts that apply are those of the
|
|
39
|
+
* same kind with an `about` term in the fact's sentence. Grounded when one of them has its value;
|
|
40
|
+
* contradicted when some apply and none has it; null when none applies.
|
|
41
|
+
*/
|
|
42
|
+
export declare function checkAgainstPacks(text: string, f: Fact, packs: readonly ReferencePack[]): ReferenceVerdict;
|
|
43
|
+
/** §10: the text facts an answer touches (an `about` term anywhere in it), for a grounding service. */
|
|
44
|
+
export declare function referenceContext(text: string, packs: readonly ReferencePack[]): Array<{
|
|
45
|
+
pack: string;
|
|
46
|
+
id: string;
|
|
47
|
+
text: string;
|
|
48
|
+
source?: string;
|
|
49
|
+
}>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Reference packs (spec/findings.md §10, H4): the facts an organisation owns (prices, fees, rules,
|
|
2
|
+
// deadlines, limits, terms), loaded once and checked against every answer, even when the agent
|
|
3
|
+
// called no tool. Any industry: a pack is data, and a tenant can have its own. Mirrors
|
|
4
|
+
// sdks/python/src/zanii_blackbox/analysis/reference.py; pinned by spec/vectors/reference.json.
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { canonical } from "../reconcile/shared.js";
|
|
7
|
+
import { canonicalNumber, normaliseValue, western } from "./hallucination.js";
|
|
8
|
+
const KINDS = new Set(["money", "percent", "quantity", "date", "text"]);
|
|
9
|
+
const NUMERIC = /^[0-9]{1,3}(?:,[0-9]{3})+(?:\.[0-9]+)?$|^[0-9]+(?:\.[0-9]+)?$/;
|
|
10
|
+
const DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
11
|
+
const str = (v, max) => typeof v === "string" && v.length >= 1 && v.length <= max;
|
|
12
|
+
/** spec/findings.md §10: the packs in a reference file, checked; throws with the first problem. */
|
|
13
|
+
export function loadReferencePacks(bytes) {
|
|
14
|
+
let doc;
|
|
15
|
+
try {
|
|
16
|
+
doc = JSON.parse(new TextDecoder().decode(bytes));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error("the reference file isn't JSON");
|
|
20
|
+
}
|
|
21
|
+
const packs = doc?.packs;
|
|
22
|
+
if (!Array.isArray(packs) || packs.length < 1 || packs.length > 100)
|
|
23
|
+
throw new Error("packs must be a list of 1-100 packs");
|
|
24
|
+
const ids = new Set();
|
|
25
|
+
return packs.map((p, i) => {
|
|
26
|
+
const at = `packs[${i}]`;
|
|
27
|
+
if (typeof p !== "object" || p === null || Array.isArray(p))
|
|
28
|
+
throw new Error(`${at} must be an object`);
|
|
29
|
+
const o = p;
|
|
30
|
+
if (typeof o.id !== "string" || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(o.id))
|
|
31
|
+
throw new Error(`${at}.id must be 1-64 of a-z 0-9 -`);
|
|
32
|
+
const key = `${o.tenant ?? ""}\u0000${o.id}`;
|
|
33
|
+
if (ids.has(key))
|
|
34
|
+
throw new Error(`${at}.id ${o.id} is there twice`);
|
|
35
|
+
ids.add(key);
|
|
36
|
+
if (!str(o.version, 64))
|
|
37
|
+
throw new Error(`${at}.version must be 1-64 characters`);
|
|
38
|
+
if (o.tenant !== undefined && !str(o.tenant, 128))
|
|
39
|
+
throw new Error(`${at}.tenant must be 1-128 characters`);
|
|
40
|
+
if (o.title !== undefined && !str(o.title, 200))
|
|
41
|
+
throw new Error(`${at}.title must be 1-200 characters`);
|
|
42
|
+
if (!Array.isArray(o.facts) || o.facts.length < 1 || o.facts.length > 10_000)
|
|
43
|
+
throw new Error(`${at}.facts must be a list of 1-10,000 facts`);
|
|
44
|
+
const factIds = new Set();
|
|
45
|
+
const facts = o.facts.map((f, j) => checkFact(f, `${at}.facts[${j}]`, factIds));
|
|
46
|
+
const pack = {
|
|
47
|
+
id: o.id,
|
|
48
|
+
version: o.version,
|
|
49
|
+
...(o.tenant !== undefined ? { tenant: o.tenant } : {}),
|
|
50
|
+
...(o.title !== undefined ? { title: o.title } : {}),
|
|
51
|
+
facts,
|
|
52
|
+
};
|
|
53
|
+
return {
|
|
54
|
+
...pack,
|
|
55
|
+
hash: `sha256:${createHash("sha256").update(canonical(pack)).digest("hex")}`,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function checkFact(f, at, ids) {
|
|
60
|
+
if (typeof f !== "object" || f === null || Array.isArray(f))
|
|
61
|
+
throw new Error(`${at} must be an object`);
|
|
62
|
+
const o = f;
|
|
63
|
+
if (typeof o.id !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(o.id))
|
|
64
|
+
throw new Error(`${at}.id must be 1-128 of A-Z a-z 0-9 . _ -`);
|
|
65
|
+
if (ids.has(o.id))
|
|
66
|
+
throw new Error(`${at}.id ${o.id} is there twice`);
|
|
67
|
+
ids.add(o.id);
|
|
68
|
+
if (!Array.isArray(o.about) ||
|
|
69
|
+
o.about.length < 1 ||
|
|
70
|
+
o.about.length > 20 ||
|
|
71
|
+
!o.about.every((a) => str(a, 100)))
|
|
72
|
+
throw new Error(`${at}.about must be 1-20 terms of 1-100 characters`);
|
|
73
|
+
if (typeof o.kind !== "string" || !KINDS.has(o.kind))
|
|
74
|
+
throw new Error(`${at}.kind must be money, percent, quantity, date or text`);
|
|
75
|
+
for (const k of ["currency", "unit"])
|
|
76
|
+
if (o[k] !== undefined && !str(o[k], 32))
|
|
77
|
+
throw new Error(`${at}.${k} must be 1-32 characters`);
|
|
78
|
+
if (o.text !== undefined && !str(o.text, 2000))
|
|
79
|
+
throw new Error(`${at}.text must be 1-2,000 characters`);
|
|
80
|
+
if (o.source !== undefined && !str(o.source, 500))
|
|
81
|
+
throw new Error(`${at}.source must be 1-500 characters`);
|
|
82
|
+
let value;
|
|
83
|
+
if (o.kind === "text") {
|
|
84
|
+
if (o.text === undefined)
|
|
85
|
+
throw new Error(`${at}: a text fact needs text`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const v = typeof o.value === "number" ? String(o.value) : o.value;
|
|
89
|
+
if (typeof v !== "string")
|
|
90
|
+
throw new Error(`${at}.value is required`);
|
|
91
|
+
if (o.kind === "date") {
|
|
92
|
+
if (!DATE.test(v))
|
|
93
|
+
throw new Error(`${at}.value must be a date (YYYY-MM-DD)`);
|
|
94
|
+
value = normaliseValue(v);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
if (!NUMERIC.test(v))
|
|
98
|
+
throw new Error(`${at}.value must be a number`);
|
|
99
|
+
value = canonicalNumber(v);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
id: o.id,
|
|
104
|
+
about: o.about,
|
|
105
|
+
kind: o.kind,
|
|
106
|
+
...(value !== undefined ? { value } : {}),
|
|
107
|
+
...(o.currency !== undefined ? { currency: o.currency } : {}),
|
|
108
|
+
...(o.unit !== undefined ? { unit: o.unit } : {}),
|
|
109
|
+
...(o.text !== undefined ? { text: o.text } : {}),
|
|
110
|
+
...(o.source !== undefined ? { source: o.source } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** The packs a session's tenant sees: its own, and those for everyone. */
|
|
114
|
+
export function packsFor(packs, tenant) {
|
|
115
|
+
return packs.filter((p) => p.tenant === undefined || p.tenant === tenant);
|
|
116
|
+
}
|
|
117
|
+
const norm = (s) => western(s).toLowerCase();
|
|
118
|
+
/** The sentence around [at, end) in `text`. */
|
|
119
|
+
function sentenceAt(text, at, end) {
|
|
120
|
+
let a = at;
|
|
121
|
+
while (a > 0 && !".!?؟\n".includes(text[a - 1]))
|
|
122
|
+
a--;
|
|
123
|
+
let b = end;
|
|
124
|
+
// a decimal point between digits doesn't end a sentence
|
|
125
|
+
while (b < text.length &&
|
|
126
|
+
!(".!?؟\n".includes(text[b]) && !/[0-9]/.test(text[b + 1] ?? "")))
|
|
127
|
+
b++;
|
|
128
|
+
return text.slice(a, b);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* §10: what the packs say about one fact of an answer. The pack facts that apply are those of the
|
|
132
|
+
* same kind with an `about` term in the fact's sentence. Grounded when one of them has its value;
|
|
133
|
+
* contradicted when some apply and none has it; null when none applies.
|
|
134
|
+
*/
|
|
135
|
+
export function checkAgainstPacks(text, f, packs) {
|
|
136
|
+
if (f.kind !== "money" && f.kind !== "percent" && f.kind !== "quantity" && f.kind !== "date")
|
|
137
|
+
return null;
|
|
138
|
+
const sentence = norm(sentenceAt(text, f.at, f.end));
|
|
139
|
+
let first = null;
|
|
140
|
+
for (const pack of packs)
|
|
141
|
+
for (const fact of pack.facts) {
|
|
142
|
+
if (fact.kind !== f.kind || !fact.about.some((t) => sentence.includes(norm(t))))
|
|
143
|
+
continue;
|
|
144
|
+
if (fact.value === f.value)
|
|
145
|
+
return { status: "grounded", pack, fact };
|
|
146
|
+
first ??= { pack, fact };
|
|
147
|
+
}
|
|
148
|
+
return first ? { status: "contradicted", ...first } : null;
|
|
149
|
+
}
|
|
150
|
+
/** §10: the text facts an answer touches (an `about` term anywhere in it), for a grounding service. */
|
|
151
|
+
export function referenceContext(text, packs) {
|
|
152
|
+
const t = norm(text);
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const pack of packs)
|
|
155
|
+
for (const fact of pack.facts)
|
|
156
|
+
if (fact.text !== undefined && fact.about.some((a) => t.includes(norm(a))) && out.length < 50)
|
|
157
|
+
out.push({
|
|
158
|
+
pack: pack.id,
|
|
159
|
+
id: fact.id,
|
|
160
|
+
text: fact.text,
|
|
161
|
+
...(fact.source ? { source: fact.source } : {}),
|
|
162
|
+
});
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const OWASP_ASI: Readonly<Record<string, string>>;
|
|
2
|
+
export declare const MITRE_ATLAS: Readonly<Record<string, string>>;
|
|
3
|
+
export declare const TAXONOMY: Readonly<Record<string, {
|
|
4
|
+
owasp?: readonly string[];
|
|
5
|
+
atlas?: readonly string[];
|
|
6
|
+
}>>;
|
|
7
|
+
export interface Taxonomy {
|
|
8
|
+
owasp: Array<{
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
}>;
|
|
12
|
+
atlas: Array<{
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
/** The OWASP ASI and MITRE ATLAS entries a finding code maps to; empty lists when none fits. */
|
|
18
|
+
export declare function taxonomyOf(code: string): Taxonomy;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Threat taxonomy per finding code (spec/findings.md §7, spec/faults/taxonomy-v1.json): OWASP Top 10
|
|
2
|
+
// for Agentic Applications 2026 and MITRE ATLAS technique ids. A test checks this table matches the
|
|
3
|
+
// file exactly. Mirrors analysis/taxonomy.py.
|
|
4
|
+
export const OWASP_ASI = {
|
|
5
|
+
ASI01: "Agent Goal Hijack",
|
|
6
|
+
ASI02: "Tool Misuse & Exploitation",
|
|
7
|
+
ASI03: "Identity & Privilege Abuse",
|
|
8
|
+
ASI04: "Agentic Supply Chain Vulnerabilities",
|
|
9
|
+
ASI05: "Unexpected Code Execution (RCE)",
|
|
10
|
+
ASI06: "Memory & Context Poisoning",
|
|
11
|
+
ASI07: "Insecure Inter-Agent Communication",
|
|
12
|
+
ASI08: "Cascading Failures",
|
|
13
|
+
ASI09: "Human-Agent Trust Exploitation",
|
|
14
|
+
ASI10: "Rogue Agents",
|
|
15
|
+
};
|
|
16
|
+
export const MITRE_ATLAS = {
|
|
17
|
+
"AML.T0034": "Cost Harvesting",
|
|
18
|
+
"AML.T0034.000": "Cost Harvesting: Excessive Queries",
|
|
19
|
+
"AML.T0034.001": "Cost Harvesting: Resource-Intensive Queries",
|
|
20
|
+
"AML.T0034.002": "Cost Harvesting: Agentic Resource Consumption",
|
|
21
|
+
"AML.T0053": "AI Agent Tool Invocation",
|
|
22
|
+
"AML.T0057": "LLM Data Leakage",
|
|
23
|
+
"AML.T0080.000": "AI Agent Context Poisoning: Memory",
|
|
24
|
+
"AML.T0086": "Exfiltration via AI Agent Tool Invocation",
|
|
25
|
+
"AML.T0091.000": "Application Access Token",
|
|
26
|
+
"AML.T0099": "AI Agent Tool Data Poisoning",
|
|
27
|
+
"AML.T0110.002": "AI Agent Tool Poisoning: Runtime Response",
|
|
28
|
+
};
|
|
29
|
+
export const TAXONOMY = {
|
|
30
|
+
ABNORMAL_RUN: { owasp: ["ASI10"] },
|
|
31
|
+
ALTERED: { owasp: ["ASI10"] },
|
|
32
|
+
AUTOMATION_SURPRISE: { owasp: ["ASI10"] },
|
|
33
|
+
BYPASS: { owasp: ["ASI10"] },
|
|
34
|
+
CALL_LIMIT: { atlas: ["AML.T0034", "AML.T0034.000"] },
|
|
35
|
+
CLAIM_UNVERIFIED: { owasp: ["ASI09"] },
|
|
36
|
+
D1_REPEAT_CALL: { atlas: ["AML.T0034.002"] },
|
|
37
|
+
D2_ACTION_LOOP: { atlas: ["AML.T0034.002"] },
|
|
38
|
+
D4_CONTEXT_BLOAT: { atlas: ["AML.T0034.001"] },
|
|
39
|
+
D6_SPEND: { atlas: ["AML.T0034", "AML.T0034.002"] },
|
|
40
|
+
DATA_CANARY_TRIPPED: { owasp: ["ASI02"], atlas: ["AML.T0086", "AML.T0057"] },
|
|
41
|
+
DATA_TO_UNAPPROVED_DESTINATION: { owasp: ["ASI02"], atlas: ["AML.T0086"] },
|
|
42
|
+
EXTRA_LOCAL: { owasp: ["ASI10"] },
|
|
43
|
+
FALSE_CLAIM: { owasp: ["ASI09", "ASI10"] },
|
|
44
|
+
FALSE_SUCCESS: { owasp: ["ASI09", "ASI10"] },
|
|
45
|
+
MISSING_LOCAL: { owasp: ["ASI10"] },
|
|
46
|
+
PLAN_DEVIATION: { owasp: ["ASI01", "ASI10"] },
|
|
47
|
+
PLAN_VIOLATION: { owasp: ["ASI02", "ASI10"], atlas: ["AML.T0053"] },
|
|
48
|
+
POLICY_AUDIT: { owasp: ["ASI02"], atlas: ["AML.T0053"] },
|
|
49
|
+
POLICY_DENIED: { owasp: ["ASI02"], atlas: ["AML.T0053"] },
|
|
50
|
+
POLICY_VIOLATION: { owasp: ["ASI02"], atlas: ["AML.T0053"] },
|
|
51
|
+
RETRY_STORM: { owasp: ["ASI08"] },
|
|
52
|
+
REVOKED_MEMORY_READ: { owasp: ["ASI06"], atlas: ["AML.T0080.000"] },
|
|
53
|
+
STERILE_VIOLATION: { owasp: ["ASI02", "ASI10"], atlas: ["AML.T0053"] },
|
|
54
|
+
TOKEN_MISUSE: { owasp: ["ASI03"], atlas: ["AML.T0091.000", "AML.T0086"] },
|
|
55
|
+
TOOL_CHANGED: { owasp: ["ASI04"], atlas: ["AML.T0110.002"] },
|
|
56
|
+
TOOL_OUTPUT_MISMATCH: { owasp: ["ASI04", "ASI06"], atlas: ["AML.T0110.002", "AML.T0099"] },
|
|
57
|
+
TOOL_WITNESS_INVALID: { owasp: ["ASI04"], atlas: ["AML.T0110.002"] },
|
|
58
|
+
};
|
|
59
|
+
/** The OWASP ASI and MITRE ATLAS entries a finding code maps to; empty lists when none fits. */
|
|
60
|
+
export function taxonomyOf(code) {
|
|
61
|
+
const t = TAXONOMY[code];
|
|
62
|
+
return {
|
|
63
|
+
owasp: (t?.owasp ?? []).map((id) => ({ id, title: OWASP_ASI[id] ?? id })),
|
|
64
|
+
atlas: (t?.atlas ?? []).map((id) => ({ id, name: MITRE_ATLAS[id] ?? id })),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createConfirmation } from "@zanii/core";
|
|
2
|
+
/** spec/approvals.md §2: what an approval's arguments are, `sha256:<hex>` of their canonical JSON
|
|
3
|
+
* (keys sorted, no whitespace). The approver sees these; the run must use them. */
|
|
4
|
+
export declare function argsHash(args: unknown): string;
|
|
1
5
|
export interface ApprovalFinding {
|
|
2
6
|
code: "APPROVAL_REJECTED" | "APPROVAL_TIMEOUT";
|
|
3
7
|
source: "approval";
|
|
@@ -9,3 +13,22 @@ export interface ApprovalFinding {
|
|
|
9
13
|
}
|
|
10
14
|
/** spec/approvals.md §3: a refused action (advisory), and one nobody confirmed in time (warning). */
|
|
11
15
|
export declare function approvalFindings(lines: readonly string[]): ApprovalFinding[];
|
|
16
|
+
/**
|
|
17
|
+
* spec/approvals.md §8: a person's yes as a Zanii action_confirmation, signed with the gateway's
|
|
18
|
+
* identity: over the hash of the event approved (the held call, or the approval request), at the
|
|
19
|
+
* decision's time. Deterministic. `error` when there is no approve decision.
|
|
20
|
+
*/
|
|
21
|
+
export declare function approvalConfirmation(lines: readonly string[], approvalId: string, gateway: {
|
|
22
|
+
did: string;
|
|
23
|
+
privateKey: Uint8Array;
|
|
24
|
+
}): {
|
|
25
|
+
confirmation: ReturnType<typeof createConfirmation>;
|
|
26
|
+
decision_seq: number;
|
|
27
|
+
action_seq: number;
|
|
28
|
+
by: unknown;
|
|
29
|
+
person?: unknown;
|
|
30
|
+
} | {
|
|
31
|
+
error: "not_found" | "not_approved";
|
|
32
|
+
};
|
|
33
|
+
/** spec/approvals.md §8: Zanii's check of an action_confirmation (the DID it names signed it). */
|
|
34
|
+
export declare const verifyConfirmation: (c: unknown) => boolean;
|
package/dist/approvals/index.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
// The two-person rule (spec/approvals.md): what the approval decisions say. Pure; mirrors
|
|
2
2
|
// sdks/python/src/zanii_blackbox/approvals.py; pinned by spec/vectors/approvals.json.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { createConfirmation, verifyConfirmation as zaniiVerifyConfirmation } from "@zanii/core";
|
|
5
|
+
import { canonical } from "../reconcile/shared.js";
|
|
6
|
+
import { hashLine } from "../verify/envelope.js";
|
|
7
|
+
/** spec/approvals.md §2: what an approval's arguments are, `sha256:<hex>` of their canonical JSON
|
|
8
|
+
* (keys sorted, no whitespace). The approver sees these; the run must use them. */
|
|
9
|
+
export function argsHash(args) {
|
|
10
|
+
return `sha256:${createHash("sha256")
|
|
11
|
+
.update(canonical(args ?? null))
|
|
12
|
+
.digest("hex")}`;
|
|
13
|
+
}
|
|
3
14
|
/** spec/approvals.md §3: a refused action (advisory), and one nobody confirmed in time (warning). */
|
|
4
15
|
export function approvalFindings(lines) {
|
|
5
16
|
const out = [];
|
|
@@ -25,3 +36,40 @@ export function approvalFindings(lines) {
|
|
|
25
36
|
}
|
|
26
37
|
return out;
|
|
27
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* spec/approvals.md §8: a person's yes as a Zanii action_confirmation, signed with the gateway's
|
|
41
|
+
* identity: over the hash of the event approved (the held call, or the approval request), at the
|
|
42
|
+
* decision's time. Deterministic. `error` when there is no approve decision.
|
|
43
|
+
*/
|
|
44
|
+
export function approvalConfirmation(lines, approvalId, gateway) {
|
|
45
|
+
const events = lines.map((l) => JSON.parse(l));
|
|
46
|
+
const decision = events.find((e) => e.kind === "control" && e.meta.action === "approval" && e.meta.approval_id === approvalId);
|
|
47
|
+
if (!decision)
|
|
48
|
+
return { error: "not_found" };
|
|
49
|
+
if (decision.meta.decision !== "approve")
|
|
50
|
+
return { error: "not_approved" };
|
|
51
|
+
const request = events.find((e) => e.kind === "control" &&
|
|
52
|
+
e.meta.action === "approval_request" &&
|
|
53
|
+
e.meta.approval_id === approvalId);
|
|
54
|
+
const actionSeq = typeof decision.meta.call_seq === "number" ? decision.meta.call_seq : (request?.seq ?? -1);
|
|
55
|
+
const line = lines[actionSeq];
|
|
56
|
+
if (line === undefined)
|
|
57
|
+
return { error: "not_found" };
|
|
58
|
+
const confirmation = createConfirmation({ ownerId: gateway.did, actionHash: hashLine(line), confirmedAt: decision.ts }, gateway.privateKey);
|
|
59
|
+
return {
|
|
60
|
+
confirmation,
|
|
61
|
+
decision_seq: decision.seq,
|
|
62
|
+
action_seq: actionSeq,
|
|
63
|
+
by: decision.meta.by,
|
|
64
|
+
...(decision.meta.person !== undefined ? { person: decision.meta.person } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** spec/approvals.md §8: Zanii's check of an action_confirmation (the DID it names signed it). */
|
|
68
|
+
export const verifyConfirmation = (c) => {
|
|
69
|
+
try {
|
|
70
|
+
return zaniiVerifyConfirmation(c);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface ArchiveFile {
|
|
2
|
+
name: string;
|
|
3
|
+
sha256: string;
|
|
4
|
+
bytes: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ArchiveManifest {
|
|
7
|
+
v: 1;
|
|
8
|
+
id: string;
|
|
9
|
+
created_at: string;
|
|
10
|
+
files: ArchiveFile[];
|
|
11
|
+
/** RFC 9162 root over leafHash(`<name> <sha256>`), in name order. */
|
|
12
|
+
root: string;
|
|
13
|
+
/** The log checkpoint (a signed note) when it was made, or null. */
|
|
14
|
+
checkpoint: string | null;
|
|
15
|
+
/** RFC 3161 TimeStampResps, base64, oldest first. */
|
|
16
|
+
timestamps: string[];
|
|
17
|
+
}
|
|
18
|
+
/** §2: one file's entry. Names are plain: 1-128 of [A-Za-z0-9._-]. */
|
|
19
|
+
export declare function archiveEntry(name: string, data: Uint8Array): ArchiveFile;
|
|
20
|
+
/** §2: the manifest, without timestamps, for these entries (unique names). */
|
|
21
|
+
export declare function archiveManifest(id: string, createdAt: string, entries: readonly ArchiveFile[], checkpoint: string | null): ArchiveManifest;
|
|
22
|
+
/** §3: what the `n`th timestamp stamps: SHA-256 of the manifest's core, then of that and the previous token. */
|
|
23
|
+
export declare function archiveDigest(manifest: ArchiveManifest, n: number): Uint8Array;
|
|
24
|
+
export interface ArchiveReport {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
/** Each file's hash and size match; null when no files were given. */
|
|
27
|
+
files: boolean | null;
|
|
28
|
+
root: boolean;
|
|
29
|
+
/** Each timestamp's time, oldest first, while the chain holds. */
|
|
30
|
+
timestamps: string[];
|
|
31
|
+
problems: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* §4: checks a manifest offline: its root, each file given (by name), and the timestamp chain,
|
|
35
|
+
* each token against the digest it must stamp and `tsaRoots`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function verifyArchive(manifest: ArchiveManifest, files: ReadonlyMap<string, Uint8Array> | null, options?: {
|
|
38
|
+
tsaRoots?: readonly string[];
|
|
39
|
+
}): ArchiveReport;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Cold archives (spec/archive.md): a set of closed sessions' records, kept for years as plain JSONL
|
|
2
|
+
// files with a manifest: each file's SHA-256, an RFC 9162 Merkle root over them, the log checkpoint
|
|
3
|
+
// they sit under, and a chain of RFC 3161 timestamps. Each renewal stamps the manifest and the
|
|
4
|
+
// previous token, so the evidence outlives the TSA's certificate and its algorithms (the RFC 4998
|
|
5
|
+
// idea). Mirrors sdks/python/src/zanii_blackbox/archive.py.
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { canonicalBytes } from "@zanii/core";
|
|
8
|
+
import { verifyTimestamp } from "../timestamp/index.js";
|
|
9
|
+
import { leafHash, merkleRoot } from "../transparency/index.js";
|
|
10
|
+
const sha256 = (...parts) => {
|
|
11
|
+
const h = createHash("sha256");
|
|
12
|
+
for (const p of parts)
|
|
13
|
+
h.update(p);
|
|
14
|
+
return new Uint8Array(h.digest());
|
|
15
|
+
};
|
|
16
|
+
const hex = (b) => Buffer.from(b).toString("hex");
|
|
17
|
+
const NAME = /^[A-Za-z0-9._-]{1,128}$/;
|
|
18
|
+
/** §2: one file's entry. Names are plain: 1-128 of [A-Za-z0-9._-]. */
|
|
19
|
+
export function archiveEntry(name, data) {
|
|
20
|
+
if (!NAME.test(name))
|
|
21
|
+
throw new Error(`an archive file name must be 1-128 of [A-Za-z0-9._-]: ${name}`);
|
|
22
|
+
return { name, sha256: hex(sha256(data)), bytes: data.length };
|
|
23
|
+
}
|
|
24
|
+
/** §2: the manifest, without timestamps, for these entries (unique names). */
|
|
25
|
+
export function archiveManifest(id, createdAt, entries, checkpoint) {
|
|
26
|
+
const names = new Set(entries.map((f) => f.name));
|
|
27
|
+
if (names.size !== entries.length)
|
|
28
|
+
throw new Error("archive file names must be unique");
|
|
29
|
+
const listed = [...entries]
|
|
30
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
31
|
+
.map((f) => ({ name: f.name, sha256: f.sha256, bytes: f.bytes }));
|
|
32
|
+
return {
|
|
33
|
+
v: 1,
|
|
34
|
+
id,
|
|
35
|
+
created_at: createdAt,
|
|
36
|
+
files: listed,
|
|
37
|
+
root: hex(merkleRoot(listed.map((f) => leafHash(Buffer.from(`${f.name} ${f.sha256}`, "utf8"))))),
|
|
38
|
+
checkpoint,
|
|
39
|
+
timestamps: [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** §3: what the `n`th timestamp stamps: SHA-256 of the manifest's core, then of that and the previous token. */
|
|
43
|
+
export function archiveDigest(manifest, n) {
|
|
44
|
+
const { timestamps, ...core } = manifest;
|
|
45
|
+
const first = sha256(canonicalBytes(core));
|
|
46
|
+
if (n === 0)
|
|
47
|
+
return first;
|
|
48
|
+
const previous = timestamps[n - 1];
|
|
49
|
+
if (previous === undefined)
|
|
50
|
+
throw new Error(`the archive has no timestamp ${n - 1}`);
|
|
51
|
+
return sha256(first, Buffer.from(previous, "base64"));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* §4: checks a manifest offline: its root, each file given (by name), and the timestamp chain,
|
|
55
|
+
* each token against the digest it must stamp and `tsaRoots`.
|
|
56
|
+
*/
|
|
57
|
+
export function verifyArchive(manifest, files, options = {}) {
|
|
58
|
+
const report = {
|
|
59
|
+
ok: false,
|
|
60
|
+
files: null,
|
|
61
|
+
root: false,
|
|
62
|
+
timestamps: [],
|
|
63
|
+
problems: [],
|
|
64
|
+
};
|
|
65
|
+
const leaves = manifest.files.map((f) => leafHash(Buffer.from(`${f.name} ${f.sha256}`, "utf8")));
|
|
66
|
+
report.root = leaves.length > 0 && hex(merkleRoot(leaves)) === manifest.root;
|
|
67
|
+
if (!report.root)
|
|
68
|
+
report.problems.push("the root isn't the files'");
|
|
69
|
+
if (files) {
|
|
70
|
+
const bad = manifest.files.filter((f) => {
|
|
71
|
+
const data = files.get(f.name);
|
|
72
|
+
return !data || data.length !== f.bytes || hex(sha256(data)) !== f.sha256;
|
|
73
|
+
});
|
|
74
|
+
report.files = bad.length === 0;
|
|
75
|
+
for (const f of bad)
|
|
76
|
+
report.problems.push(`${f.name} is missing or changed`);
|
|
77
|
+
}
|
|
78
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
79
|
+
for (let i = 0; i < manifest.timestamps.length; i++) {
|
|
80
|
+
const t = verifyTimestamp(new Uint8Array(Buffer.from(manifest.timestamps[i], "base64")), {
|
|
81
|
+
digest: archiveDigest(manifest, i),
|
|
82
|
+
...(options.tsaRoots ? { roots: options.tsaRoots } : {}),
|
|
83
|
+
});
|
|
84
|
+
const at = t.gen_time ? Date.parse(t.gen_time) : Number.NaN;
|
|
85
|
+
if (!t.ok || !t.gen_time || !(at >= last)) {
|
|
86
|
+
report.problems.push(`timestamp ${i} doesn't hold: ${t.problems.join("; ") || "out of order"}`);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
last = at;
|
|
90
|
+
report.timestamps.push(t.gen_time);
|
|
91
|
+
}
|
|
92
|
+
if (manifest.timestamps.length === 0)
|
|
93
|
+
report.problems.push("the archive has no timestamp");
|
|
94
|
+
report.ok = report.problems.length === 0;
|
|
95
|
+
return report;
|
|
96
|
+
}
|