@zanii/blackbox 0.0.0-stage → 0.2.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 +202 -0
- package/README.md +109 -2
- package/dist/agents/index.d.ts +34 -0
- package/dist/agents/index.js +73 -0
- package/dist/analysis/detectors.d.ts +36 -0
- package/dist/analysis/detectors.js +339 -0
- package/dist/analysis/faults.d.ts +9 -0
- package/dist/analysis/faults.js +250 -0
- package/dist/analysis/index.d.ts +68 -0
- package/dist/analysis/index.js +388 -0
- package/dist/analysis/landing.d.ts +25 -0
- package/dist/analysis/landing.js +225 -0
- package/dist/analysis/memory.d.ts +13 -0
- package/dist/analysis/memory.js +33 -0
- package/dist/analysis/waste.d.ts +29 -0
- package/dist/analysis/waste.js +79 -0
- package/dist/approvals/index.d.ts +11 -0
- package/dist/approvals/index.js +27 -0
- package/dist/approvals/warnings.d.ts +2 -0
- package/dist/approvals/warnings.js +28 -0
- package/dist/attest/index.d.ts +17 -0
- package/dist/attest/index.js +106 -0
- package/dist/authority/index.d.ts +24 -0
- package/dist/authority/index.js +77 -0
- package/dist/billing/index.d.ts +99 -0
- package/dist/billing/index.js +174 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1057 -0
- package/dist/client/index.d.ts +146 -0
- package/dist/client/index.js +210 -0
- package/dist/compliance/index.d.ts +41 -0
- package/dist/compliance/index.js +96 -0
- package/dist/cost/index.d.ts +133 -0
- package/dist/cost/index.js +293 -0
- package/dist/data/index.d.ts +191 -0
- package/dist/data/index.js +762 -0
- package/dist/directives/index.d.ts +35 -0
- package/dist/directives/index.js +80 -0
- package/dist/drills/index.d.ts +43 -0
- package/dist/drills/index.js +101 -0
- package/dist/duty/index.d.ts +21 -0
- package/dist/duty/index.js +68 -0
- package/dist/fleet/index.d.ts +141 -0
- package/dist/fleet/index.js +454 -0
- package/dist/hooks/ai-sdk.d.ts +42 -0
- package/dist/hooks/ai-sdk.js +62 -0
- package/dist/hooks/claude-agent-sdk.d.ts +14 -0
- package/dist/hooks/claude-agent-sdk.js +70 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.js +10 -0
- package/dist/hooks/langchain-agent.d.ts +69 -0
- package/dist/hooks/langchain-agent.js +163 -0
- package/dist/hooks/langchain.d.ts +41 -0
- package/dist/hooks/langchain.js +216 -0
- package/dist/hooks/langgraph-checkpoint.d.ts +12 -0
- package/dist/hooks/langgraph-checkpoint.js +73 -0
- package/dist/hooks/memory.d.ts +17 -0
- package/dist/hooks/memory.js +64 -0
- package/dist/hooks/openai-agents.d.ts +6 -0
- package/dist/hooks/openai-agents.js +40 -0
- package/dist/hooks/protect.d.ts +7 -0
- package/dist/hooks/protect.js +39 -0
- package/dist/hooks/providers.d.ts +16 -0
- package/dist/hooks/providers.js +149 -0
- package/dist/hooks/shared.d.ts +11 -0
- package/dist/hooks/shared.js +39 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +48 -0
- package/dist/investigate/index.d.ts +66 -0
- package/dist/investigate/index.js +119 -0
- package/dist/mcp-server/index.d.ts +85 -0
- package/dist/mcp-server/index.js +216 -0
- package/dist/mcp-wrap/index.d.ts +17 -0
- package/dist/mcp-wrap/index.js +170 -0
- package/dist/money/index.d.ts +114 -0
- package/dist/money/index.js +622 -0
- package/dist/occurrence/index.d.ts +108 -0
- package/dist/occurrence/index.js +168 -0
- package/dist/ocsf/index.d.ts +22 -0
- package/dist/ocsf/index.js +168 -0
- package/dist/otlp/index.d.ts +24 -0
- package/dist/otlp/index.js +143 -0
- package/dist/packs/index.d.ts +48 -0
- package/dist/packs/index.js +343 -0
- package/dist/policy/delta.d.ts +11 -0
- package/dist/policy/delta.js +39 -0
- package/dist/policy/drafts.d.ts +34 -0
- package/dist/policy/drafts.js +129 -0
- package/dist/policy/index.d.ts +47 -0
- package/dist/policy/index.js +154 -0
- package/dist/precog/index.d.ts +96 -0
- package/dist/precog/index.js +167 -0
- package/dist/precog/intervention.d.ts +22 -0
- package/dist/precog/intervention.js +44 -0
- package/dist/precog/normal.d.ts +31 -0
- package/dist/precog/normal.js +89 -0
- package/dist/preflight/index.d.ts +11 -0
- package/dist/preflight/index.js +19 -0
- package/dist/ratings/index.d.ts +21 -0
- package/dist/ratings/index.js +48 -0
- package/dist/reconcile/claude-code.d.ts +19 -0
- package/dist/reconcile/claude-code.js +220 -0
- package/dist/reconcile/codex.d.ts +5 -0
- package/dist/reconcile/codex.js +191 -0
- package/dist/reconcile/index.d.ts +19 -0
- package/dist/reconcile/index.js +50 -0
- package/dist/reconcile/record.d.ts +49 -0
- package/dist/reconcile/record.js +225 -0
- package/dist/reconcile/shared.d.ts +65 -0
- package/dist/reconcile/shared.js +113 -0
- package/dist/replay/index.d.ts +11 -0
- package/dist/replay/index.js +64 -0
- package/dist/replay/repair.d.ts +10 -0
- package/dist/replay/repair.js +62 -0
- package/dist/session/drain.d.ts +13 -0
- package/dist/session/drain.js +35 -0
- package/dist/session/index.d.ts +275 -0
- package/dist/session/index.js +681 -0
- package/dist/undo/index.d.ts +45 -0
- package/dist/undo/index.js +212 -0
- package/dist/verify/anchor.d.ts +54 -0
- package/dist/verify/anchor.js +77 -0
- package/dist/verify/chain.d.ts +27 -0
- package/dist/verify/chain.js +105 -0
- package/dist/verify/envelope.d.ts +28 -0
- package/dist/verify/envelope.js +55 -0
- package/dist/verify/index.d.ts +3 -0
- package/dist/verify/index.js +3 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/weather/index.d.ts +24 -0
- package/dist/weather/index.js +45 -0
- package/dist/workspace-receipt/index.d.ts +15 -0
- package/dist/workspace-receipt/index.js +121 -0
- package/package.json +56 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { LoadedPrices } from "../cost/index.ts";
|
|
2
|
+
import { type Summary } from "../fleet/index.ts";
|
|
3
|
+
import type { Bodies } from "../reconcile/record.ts";
|
|
4
|
+
export interface OccurrenceFramework {
|
|
5
|
+
title: string;
|
|
6
|
+
authority: string;
|
|
7
|
+
deadlines: string[];
|
|
8
|
+
sections: Array<{
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
fill: "record" | "findings" | "redactions" | "operator";
|
|
12
|
+
}>;
|
|
13
|
+
/** spec/packs.md §1: the Arabic title, authority, deadlines and section titles (by id). */
|
|
14
|
+
ar?: {
|
|
15
|
+
title: string;
|
|
16
|
+
authority: string;
|
|
17
|
+
deadlines: string[];
|
|
18
|
+
sections: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface OccurrenceFacts {
|
|
22
|
+
summary: Summary;
|
|
23
|
+
findings: Array<{
|
|
24
|
+
seq: number;
|
|
25
|
+
ts: string;
|
|
26
|
+
code: string;
|
|
27
|
+
fault: string | null;
|
|
28
|
+
severity: string;
|
|
29
|
+
}>;
|
|
30
|
+
redactions: Record<string, number>;
|
|
31
|
+
verified: boolean;
|
|
32
|
+
generated_at: string;
|
|
33
|
+
}
|
|
34
|
+
/** The facts a report is filled from (the servers pass `verified` from their own verification). */
|
|
35
|
+
export declare function occurrenceFacts(lines: readonly string[], bodies: Bodies, prices: LoadedPrices, verified: boolean, generatedAt: string): OccurrenceFacts;
|
|
36
|
+
/** A report in English, or in Arabic (`lang` "ar") from the framework's `ar`; throws when it has none. */
|
|
37
|
+
export declare function occurrenceReport(id: string, framework: OccurrenceFramework, facts: OccurrenceFacts, lang?: "en" | "ar"): {
|
|
38
|
+
v: 1;
|
|
39
|
+
framework: string;
|
|
40
|
+
lang: "ar";
|
|
41
|
+
title: string;
|
|
42
|
+
authority: string;
|
|
43
|
+
deadlines: string[];
|
|
44
|
+
session_id: string;
|
|
45
|
+
verified: boolean;
|
|
46
|
+
generated_at: string;
|
|
47
|
+
sections: {
|
|
48
|
+
id: string;
|
|
49
|
+
title: string;
|
|
50
|
+
fill: "findings" | "operator" | "record" | "redactions";
|
|
51
|
+
text: string;
|
|
52
|
+
}[];
|
|
53
|
+
} | {
|
|
54
|
+
lang?: never;
|
|
55
|
+
v: 1;
|
|
56
|
+
framework: string;
|
|
57
|
+
title: string;
|
|
58
|
+
authority: string;
|
|
59
|
+
deadlines: string[];
|
|
60
|
+
session_id: string;
|
|
61
|
+
verified: boolean;
|
|
62
|
+
generated_at: string;
|
|
63
|
+
sections: {
|
|
64
|
+
id: string;
|
|
65
|
+
title: string;
|
|
66
|
+
fill: "findings" | "operator" | "record" | "redactions";
|
|
67
|
+
text: string;
|
|
68
|
+
}[];
|
|
69
|
+
};
|
|
70
|
+
export type OccurrenceReport = ReturnType<typeof occurrenceReport>;
|
|
71
|
+
export declare function renderOccurrence(r: OccurrenceReport, notes: string): string;
|
|
72
|
+
/** spec/occurrence.md "Filing": what a person needs to file the report in an authority's portal. */
|
|
73
|
+
export declare function filingPack(r: OccurrenceReport, notes: string, pack: {
|
|
74
|
+
id: string;
|
|
75
|
+
status: string;
|
|
76
|
+
}, evidence: {
|
|
77
|
+
events: number;
|
|
78
|
+
head: string | null;
|
|
79
|
+
anchored_events: number;
|
|
80
|
+
}): {
|
|
81
|
+
v: 1;
|
|
82
|
+
framework: string;
|
|
83
|
+
pack: {
|
|
84
|
+
id: string;
|
|
85
|
+
status: string;
|
|
86
|
+
};
|
|
87
|
+
title: string;
|
|
88
|
+
authority: string;
|
|
89
|
+
deadlines: string[];
|
|
90
|
+
session_id: string;
|
|
91
|
+
verified: boolean;
|
|
92
|
+
generated_at: string;
|
|
93
|
+
fields: {
|
|
94
|
+
id: string;
|
|
95
|
+
title: string;
|
|
96
|
+
text: string;
|
|
97
|
+
complete: boolean;
|
|
98
|
+
}[];
|
|
99
|
+
to_complete: string[];
|
|
100
|
+
evidence: {
|
|
101
|
+
events: number;
|
|
102
|
+
head: string | null;
|
|
103
|
+
anchored_events: number;
|
|
104
|
+
bundle: string;
|
|
105
|
+
verify: string;
|
|
106
|
+
};
|
|
107
|
+
markdown: string;
|
|
108
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Regulator occurrence reports (spec/occurrence.md). Mirrors sdks/python/src/zanii_blackbox/occurrence.py.
|
|
2
|
+
import { sessionSummary } from "../fleet/index.js";
|
|
3
|
+
/** The facts a report is filled from (the servers pass `verified` from their own verification). */
|
|
4
|
+
export function occurrenceFacts(lines, bodies, prices, verified, generatedAt) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
const redactions = {};
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
const e = JSON.parse(line);
|
|
9
|
+
if (e.kind === "finding")
|
|
10
|
+
findings.push({
|
|
11
|
+
seq: e.seq,
|
|
12
|
+
ts: e.ts,
|
|
13
|
+
code: String(e.meta.code),
|
|
14
|
+
fault: typeof e.meta.fault === "string" ? e.meta.fault : null,
|
|
15
|
+
severity: String(e.meta.severity),
|
|
16
|
+
});
|
|
17
|
+
const r = e.meta.redacted;
|
|
18
|
+
if (typeof r === "object" && r !== null)
|
|
19
|
+
for (const [k, v] of Object.entries(r))
|
|
20
|
+
if (typeof v === "number")
|
|
21
|
+
redactions[k] = (redactions[k] ?? 0) + v;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
summary: sessionSummary(lines, bodies, prices),
|
|
25
|
+
findings,
|
|
26
|
+
redactions: Object.fromEntries(Object.keys(redactions)
|
|
27
|
+
.sort()
|
|
28
|
+
.map((k) => [k, redactions[k]])),
|
|
29
|
+
verified,
|
|
30
|
+
generated_at: generatedAt,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function textOf(id, fill, f) {
|
|
34
|
+
const s = f.summary;
|
|
35
|
+
if (fill === "operator")
|
|
36
|
+
return "[To be completed by the operator]";
|
|
37
|
+
if (fill === "redactions") {
|
|
38
|
+
const kinds = Object.entries(f.redactions);
|
|
39
|
+
return kinds.length
|
|
40
|
+
? `Redacted before storage: ${kinds.map(([k, v]) => `${k} × ${v}`).join(", ")}.`
|
|
41
|
+
: "No personal data patterns were redacted from this record.";
|
|
42
|
+
}
|
|
43
|
+
if (fill === "findings") {
|
|
44
|
+
const bad = f.findings.filter((x) => x.severity === "warning" || x.severity === "caution");
|
|
45
|
+
return bad.length
|
|
46
|
+
? bad.map((x) => `- ${x.ts} · ${x.fault ?? "—"} · ${x.severity} · ${x.code}`).join("\n")
|
|
47
|
+
: "No warning or caution findings in the record.";
|
|
48
|
+
}
|
|
49
|
+
if (id === "timeline")
|
|
50
|
+
return `From ${s.started_at} to ${s.last_event_at} (the gateway's clock).`;
|
|
51
|
+
const status = s.blocked_by ? `blocked by ${s.blocked_by}` : s.closed ? "closed" : "open";
|
|
52
|
+
return `Agent session ${s.label ?? s.session_id} (${s.session_id}): ${s.model_calls} model calls, ${s.tool_calls} tool calls, outcome ${s.outcome ?? "none"}, status ${status}.`;
|
|
53
|
+
}
|
|
54
|
+
// spec/occurrence.md "Arabic": the same facts in Arabic words; ids, times and codes stay as they are.
|
|
55
|
+
function textOfAr(id, fill, f) {
|
|
56
|
+
const s = f.summary;
|
|
57
|
+
if (fill === "operator")
|
|
58
|
+
return "[يستكمله المشغّل]";
|
|
59
|
+
if (fill === "redactions") {
|
|
60
|
+
const kinds = Object.entries(f.redactions);
|
|
61
|
+
return kinds.length
|
|
62
|
+
? `حُجب قبل التخزين: ${kinds.map(([k, v]) => `${k} × ${v}`).join("، ")}.`
|
|
63
|
+
: "لم يُحجب أي نمط من أنماط البيانات الشخصية في هذا السجل.";
|
|
64
|
+
}
|
|
65
|
+
if (fill === "findings") {
|
|
66
|
+
const bad = f.findings.filter((x) => x.severity === "warning" || x.severity === "caution");
|
|
67
|
+
return bad.length
|
|
68
|
+
? bad.map((x) => `- ${x.ts} · ${x.fault ?? "—"} · ${x.severity} · ${x.code}`).join("\n")
|
|
69
|
+
: "لا توجد في السجل نتائج بمستوى تحذير أو تنبيه.";
|
|
70
|
+
}
|
|
71
|
+
if (id === "timeline")
|
|
72
|
+
return `من ${s.started_at} إلى ${s.last_event_at} (بحسب ساعة البوابة).`;
|
|
73
|
+
const status = s.blocked_by ? `محظورة بواسطة ${s.blocked_by}` : s.closed ? "مغلقة" : "مفتوحة";
|
|
74
|
+
return `جلسة الوكيل ${s.label ?? s.session_id} (${s.session_id}): ${s.model_calls} من استدعاءات النموذج، ${s.tool_calls} من استدعاءات الأدوات، النتيجة ${s.outcome ?? "لا شيء"}، الحالة ${status}.`;
|
|
75
|
+
}
|
|
76
|
+
/** A report in English, or in Arabic (`lang` "ar") from the framework's `ar`; throws when it has none. */
|
|
77
|
+
export function occurrenceReport(id, framework, facts, lang = "en") {
|
|
78
|
+
if (lang === "ar") {
|
|
79
|
+
const ar = framework.ar;
|
|
80
|
+
if (!ar)
|
|
81
|
+
throw new Error(`framework ${id} has no Arabic`);
|
|
82
|
+
return {
|
|
83
|
+
v: 1,
|
|
84
|
+
framework: id,
|
|
85
|
+
lang: "ar",
|
|
86
|
+
title: ar.title,
|
|
87
|
+
authority: ar.authority,
|
|
88
|
+
deadlines: [...ar.deadlines],
|
|
89
|
+
session_id: facts.summary.session_id,
|
|
90
|
+
verified: facts.verified,
|
|
91
|
+
generated_at: facts.generated_at,
|
|
92
|
+
sections: framework.sections.map((x) => ({
|
|
93
|
+
id: x.id,
|
|
94
|
+
title: ar.sections[x.id],
|
|
95
|
+
fill: x.fill,
|
|
96
|
+
text: textOfAr(x.id, x.fill, facts),
|
|
97
|
+
})),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
v: 1,
|
|
102
|
+
framework: id,
|
|
103
|
+
title: framework.title,
|
|
104
|
+
authority: framework.authority,
|
|
105
|
+
deadlines: [...framework.deadlines],
|
|
106
|
+
session_id: facts.summary.session_id,
|
|
107
|
+
verified: facts.verified,
|
|
108
|
+
generated_at: facts.generated_at,
|
|
109
|
+
sections: framework.sections.map((x) => ({
|
|
110
|
+
id: x.id,
|
|
111
|
+
title: x.title,
|
|
112
|
+
fill: x.fill,
|
|
113
|
+
text: textOf(x.id, x.fill, facts),
|
|
114
|
+
})),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const LABELS = {
|
|
118
|
+
en: ["Session", "Record verifies", "yes", "NO", "Report to", "Generated", "Deadlines"],
|
|
119
|
+
ar: ["الجلسة", "السجل موثّق", "نعم", "لا", "جهة الإبلاغ", "تاريخ الإنشاء", "المواعيد النهائية"],
|
|
120
|
+
};
|
|
121
|
+
export function renderOccurrence(r, notes) {
|
|
122
|
+
const [session, verifies, yes, no, reportTo, generated, deadlines] = LABELS["lang" in r ? "ar" : "en"];
|
|
123
|
+
const out = [
|
|
124
|
+
`# ${r.title}`,
|
|
125
|
+
"",
|
|
126
|
+
"| | |",
|
|
127
|
+
"|---|---|",
|
|
128
|
+
`| ${session} | \`${r.session_id}\` |`,
|
|
129
|
+
`| ${verifies} | ${r.verified ? yes : no} |`,
|
|
130
|
+
`| ${reportTo} | ${r.authority} |`,
|
|
131
|
+
`| ${generated} | ${r.generated_at} |`,
|
|
132
|
+
"",
|
|
133
|
+
`**${deadlines}**`,
|
|
134
|
+
"",
|
|
135
|
+
...r.deadlines.map((d) => `- ${d}`),
|
|
136
|
+
...r.sections.flatMap((x) => ["", `## ${x.title}`, "", x.text]),
|
|
137
|
+
"",
|
|
138
|
+
`*${notes}*`,
|
|
139
|
+
];
|
|
140
|
+
return `${out.join("\n")}\n`;
|
|
141
|
+
}
|
|
142
|
+
/** spec/occurrence.md "Filing": what a person needs to file the report in an authority's portal. */
|
|
143
|
+
export function filingPack(r, notes, pack, evidence) {
|
|
144
|
+
return {
|
|
145
|
+
v: 1,
|
|
146
|
+
framework: r.framework,
|
|
147
|
+
pack,
|
|
148
|
+
title: r.title,
|
|
149
|
+
authority: r.authority,
|
|
150
|
+
deadlines: r.deadlines,
|
|
151
|
+
session_id: r.session_id,
|
|
152
|
+
verified: r.verified,
|
|
153
|
+
generated_at: r.generated_at,
|
|
154
|
+
fields: r.sections.map((s) => ({
|
|
155
|
+
id: s.id,
|
|
156
|
+
title: s.title,
|
|
157
|
+
text: s.text,
|
|
158
|
+
complete: s.fill !== "operator",
|
|
159
|
+
})),
|
|
160
|
+
to_complete: r.sections.filter((s) => s.fill === "operator").map((s) => s.id),
|
|
161
|
+
evidence: {
|
|
162
|
+
...evidence,
|
|
163
|
+
bundle: `/v1/sessions/${r.session_id}/bundle`,
|
|
164
|
+
verify: "blackbox verify <bundle.json> --proofs",
|
|
165
|
+
},
|
|
166
|
+
markdown: renderOccurrence(r, notes),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Json } from "../verify/index.ts";
|
|
2
|
+
export type OcsfVersion = "1.8" | "1.3" | "1.1";
|
|
3
|
+
export type OcsfEvent = {
|
|
4
|
+
[key: string]: Json;
|
|
5
|
+
};
|
|
6
|
+
/** The session's events as OCSF, oldest first. Agent-asserted SDK events aren't exported: a SIEM
|
|
7
|
+
* gets what the gateway saw. */
|
|
8
|
+
export declare function toOcsf(lines: readonly string[], options?: {
|
|
9
|
+
version?: OcsfVersion;
|
|
10
|
+
productVersion?: string;
|
|
11
|
+
}): OcsfEvent[];
|
|
12
|
+
/** One line per event, for people and grep: `<ISO time> <class>/<activity> <severity> <message>`. */
|
|
13
|
+
export declare function ocsfLine(ev: OcsfEvent): string;
|
|
14
|
+
/** Idea I4: reads an OCSF JSONL log (OpenShell's sandbox audit log, for one) into the session as
|
|
15
|
+
* `ocsf` SDK events, named by class. A record's `metadata.uid` is its stable event id, so reading
|
|
16
|
+
* the same log twice records nothing twice. Lines that aren't JSON objects are skipped; returns
|
|
17
|
+
* how many were recorded. */
|
|
18
|
+
export declare function ingestOcsf(session: {
|
|
19
|
+
event(type: string, name?: string, data?: Record<string, unknown>, o?: {
|
|
20
|
+
eventId?: string;
|
|
21
|
+
}): void;
|
|
22
|
+
}, jsonl: string): number;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// OCSF export (spec/ocsf.md, idea S1): a session's record as OCSF 1.8.0 events for a SIEM (Splunk,
|
|
2
|
+
// AWS Security Lake, CrowdStrike, Elastic), with a lossy downgrade to 1.1 / 1.3 and a one-line text
|
|
3
|
+
// form. Mirrors ocsf.py.
|
|
4
|
+
const SEVERITY = {
|
|
5
|
+
advisory: [2, "Low"],
|
|
6
|
+
caution: [3, "Medium"],
|
|
7
|
+
warning: [4, "High"],
|
|
8
|
+
};
|
|
9
|
+
function base(e, cls, activity, message, productVersion) {
|
|
10
|
+
const [classUid, className, categoryUid, categoryName] = cls;
|
|
11
|
+
return {
|
|
12
|
+
class_uid: classUid,
|
|
13
|
+
class_name: className,
|
|
14
|
+
category_uid: categoryUid,
|
|
15
|
+
category_name: categoryName,
|
|
16
|
+
activity_id: activity[0],
|
|
17
|
+
activity_name: activity[1],
|
|
18
|
+
type_uid: classUid * 100 + activity[0],
|
|
19
|
+
severity_id: 1,
|
|
20
|
+
severity: "Informational",
|
|
21
|
+
status_id: 1,
|
|
22
|
+
status: "Success",
|
|
23
|
+
time: Date.parse(e.ts),
|
|
24
|
+
message,
|
|
25
|
+
metadata: {
|
|
26
|
+
version: "1.8.0",
|
|
27
|
+
uid: `${e.session_id}:${e.seq}`,
|
|
28
|
+
correlation_uid: e.session_id,
|
|
29
|
+
log_name: "zanii-blackbox",
|
|
30
|
+
product: { name: "Zanii Blackbox", vendor_name: "Zanii", version: productVersion },
|
|
31
|
+
},
|
|
32
|
+
unmapped: { seq: e.seq, kind: e.kind },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const HTTP = [4002, "HTTP Activity", 4, "Network Activity"];
|
|
36
|
+
const API = [6003, "API Activity", 6, "Application Activity"];
|
|
37
|
+
const LIFECYCLE = [
|
|
38
|
+
6002,
|
|
39
|
+
"Application Lifecycle",
|
|
40
|
+
6,
|
|
41
|
+
"Application Activity",
|
|
42
|
+
];
|
|
43
|
+
const FINDING = [2004, "Detection Finding", 2, "Findings"];
|
|
44
|
+
/** The session's events as OCSF, oldest first. Agent-asserted SDK events aren't exported: a SIEM
|
|
45
|
+
* gets what the gateway saw. */
|
|
46
|
+
export function toOcsf(lines, options = {}) {
|
|
47
|
+
const pv = options.productVersion ?? "unknown";
|
|
48
|
+
const requests = new Map();
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
const e = JSON.parse(line);
|
|
52
|
+
const m = e.meta;
|
|
53
|
+
if (e.kind === "llm.request") {
|
|
54
|
+
requests.set(e.seq, {
|
|
55
|
+
...(typeof m.provider === "string" ? { provider: m.provider } : {}),
|
|
56
|
+
...(typeof m.method === "string" ? { method: m.method } : {}),
|
|
57
|
+
...(typeof m.path === "string" ? { path: m.path } : {}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else if (e.kind === "llm.response") {
|
|
61
|
+
const r = typeof m.request_seq === "number" ? requests.get(m.request_seq) : undefined;
|
|
62
|
+
const status = typeof m.status === "number" ? m.status : 0;
|
|
63
|
+
const model = typeof m.model === "string" ? m.model : "unknown";
|
|
64
|
+
const provider = r?.provider ?? "unknown";
|
|
65
|
+
const ev = base(e, HTTP, [6, "Post"], `${r?.method ?? "POST"} ${provider} ${model} → ${status}`, pv);
|
|
66
|
+
ev.http_request = { http_method: r?.method ?? "POST", url: { path: r?.path ?? "" } };
|
|
67
|
+
ev.http_response = { code: status };
|
|
68
|
+
ev.ai_model = { name: model, ai_provider: provider };
|
|
69
|
+
ev.metadata.profiles = ["ai_operation"];
|
|
70
|
+
if (status >= 400)
|
|
71
|
+
Object.assign(ev, { status_id: 2, status: "Failure" });
|
|
72
|
+
out.push(ev);
|
|
73
|
+
}
|
|
74
|
+
else if (e.kind === "tool.call" && m.method === "tools/call") {
|
|
75
|
+
const p = m.policy;
|
|
76
|
+
const tool = `${String(m.server ?? "?")}.${String(m.tool ?? "?")}`;
|
|
77
|
+
const denied = p?.action === "deny";
|
|
78
|
+
const ev = base(e, API, [99, "Other"], denied ? `tools/call ${tool} denied by ${String(p?.rule)}` : `tools/call ${tool}`, pv);
|
|
79
|
+
ev.api = { operation: "tools/call", service: { name: String(m.server ?? "") } };
|
|
80
|
+
Object.assign(ev, denied
|
|
81
|
+
? {
|
|
82
|
+
action_id: 2,
|
|
83
|
+
action: "Denied",
|
|
84
|
+
disposition_id: 2,
|
|
85
|
+
disposition: "Blocked",
|
|
86
|
+
status_id: 2,
|
|
87
|
+
status: "Failure",
|
|
88
|
+
firewall_rule: { name: String(p?.rule), type: "blackbox-policy" },
|
|
89
|
+
}
|
|
90
|
+
: { action_id: 1, action: "Allowed", disposition_id: 1, disposition: "Allowed" });
|
|
91
|
+
out.push(ev);
|
|
92
|
+
}
|
|
93
|
+
else if (e.kind === "finding") {
|
|
94
|
+
const [sid, sev] = SEVERITY[String(m.severity)] ?? [5, "Critical"];
|
|
95
|
+
const ev = base(e, FINDING, [1, "Create"], String(m.detail ?? m.code), pv);
|
|
96
|
+
Object.assign(ev, { severity_id: sid, severity: sev });
|
|
97
|
+
ev.finding_info = {
|
|
98
|
+
uid: `${e.session_id}:${e.seq}`,
|
|
99
|
+
title: String(m.code),
|
|
100
|
+
types: [String(m.code)],
|
|
101
|
+
};
|
|
102
|
+
ev.metadata.event_code = String(m.fault ?? m.code);
|
|
103
|
+
out.push(ev);
|
|
104
|
+
}
|
|
105
|
+
else if (e.kind === "session.open" || e.kind === "session.close") {
|
|
106
|
+
const open = e.kind === "session.open";
|
|
107
|
+
const ev = base(e, LIFECYCLE, open ? [3, "Start"] : [4, "Stop"], open ? "session opened" : "session closed", pv);
|
|
108
|
+
ev.app = { name: "Zanii Blackbox session", uid: e.session_id };
|
|
109
|
+
out.push(ev);
|
|
110
|
+
}
|
|
111
|
+
else if (e.kind === "control" && typeof m.action === "string") {
|
|
112
|
+
const ev = base(e, API, [99, "Other"], `control ${m.action}`, pv);
|
|
113
|
+
ev.api = { operation: `control.${m.action}`, service: { name: "blackbox" } };
|
|
114
|
+
out.push(ev);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return options.version && options.version !== "1.8"
|
|
118
|
+
? out.map((ev) => downgrade(ev, options.version))
|
|
119
|
+
: out;
|
|
120
|
+
}
|
|
121
|
+
/** Idea S1: for a SIEM on an older OCSF: drops what came later (`ai_model`), the `ai_operation`
|
|
122
|
+
* profile, and says so in `unmapped.downgraded_from`. */
|
|
123
|
+
function downgrade(ev, version) {
|
|
124
|
+
const { ai_model: _, ...rest } = ev;
|
|
125
|
+
const metadata = {
|
|
126
|
+
...rest.metadata,
|
|
127
|
+
version: `${version}.0`,
|
|
128
|
+
};
|
|
129
|
+
const profiles = metadata.profiles?.filter((p) => p !== "ai_operation");
|
|
130
|
+
if (profiles?.length)
|
|
131
|
+
metadata.profiles = profiles;
|
|
132
|
+
else
|
|
133
|
+
delete metadata.profiles;
|
|
134
|
+
return {
|
|
135
|
+
...rest,
|
|
136
|
+
metadata,
|
|
137
|
+
unmapped: { ...rest.unmapped, downgraded_from: "1.8.0" },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** One line per event, for people and grep: `<ISO time> <class>/<activity> <severity> <message>`. */
|
|
141
|
+
export function ocsfLine(ev) {
|
|
142
|
+
return `${new Date(ev.time).toISOString()} ${ev.class_name}/${ev.activity_name} ${ev.severity} ${ev.message}`;
|
|
143
|
+
}
|
|
144
|
+
/** Idea I4: reads an OCSF JSONL log (OpenShell's sandbox audit log, for one) into the session as
|
|
145
|
+
* `ocsf` SDK events, named by class. A record's `metadata.uid` is its stable event id, so reading
|
|
146
|
+
* the same log twice records nothing twice. Lines that aren't JSON objects are skipped; returns
|
|
147
|
+
* how many were recorded. */
|
|
148
|
+
export function ingestOcsf(session, jsonl) {
|
|
149
|
+
let n = 0;
|
|
150
|
+
for (const line of jsonl.split(/\r?\n/)) {
|
|
151
|
+
if (!line.trim())
|
|
152
|
+
continue;
|
|
153
|
+
let ev;
|
|
154
|
+
try {
|
|
155
|
+
ev = JSON.parse(line);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (typeof ev !== "object" || ev === null || Array.isArray(ev))
|
|
161
|
+
continue;
|
|
162
|
+
const e = ev;
|
|
163
|
+
const uid = e.metadata?.uid;
|
|
164
|
+
session.event("ocsf", typeof e.class_name === "string" ? e.class_name.slice(0, 256) : undefined, e, typeof uid === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(uid) ? { eventId: uid } : {});
|
|
165
|
+
n++;
|
|
166
|
+
}
|
|
167
|
+
return n;
|
|
168
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type Value = {
|
|
2
|
+
stringValue: string;
|
|
3
|
+
} | {
|
|
4
|
+
intValue: string;
|
|
5
|
+
};
|
|
6
|
+
type Attr = {
|
|
7
|
+
key: string;
|
|
8
|
+
value: Value;
|
|
9
|
+
};
|
|
10
|
+
export declare function toOtlp(lines: readonly string[]): {
|
|
11
|
+
resourceSpans: {
|
|
12
|
+
resource: {
|
|
13
|
+
attributes: Attr[];
|
|
14
|
+
};
|
|
15
|
+
scopeSpans: {
|
|
16
|
+
scope: {
|
|
17
|
+
name: string;
|
|
18
|
+
version: string;
|
|
19
|
+
};
|
|
20
|
+
spans: object[];
|
|
21
|
+
}[];
|
|
22
|
+
}[];
|
|
23
|
+
};
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// A session as OpenTelemetry traces (spec/otlp.md): OTLP/JSON with the GenAI semantic conventions.
|
|
2
|
+
// Deterministic (ids derived from the session), so both SDKs produce the same bytes. Mirrors otlp.py.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { tokensOf } from "../cost/index.js";
|
|
5
|
+
const hex = (s, n) => createHash("sha256").update(s).digest("hex").slice(0, n);
|
|
6
|
+
const nanos = (ts) => `${Date.parse(ts)}000000`;
|
|
7
|
+
const str = (key, v) => typeof v === "string" ? [{ key, value: { stringValue: v } }] : [];
|
|
8
|
+
const int = (key, v) => typeof v === "number" && Number.isSafeInteger(v) ? [{ key, value: { intValue: String(v) } }] : [];
|
|
9
|
+
export function toOtlp(lines) {
|
|
10
|
+
const events = lines.map((l) => JSON.parse(l));
|
|
11
|
+
const first = events[0];
|
|
12
|
+
if (!first)
|
|
13
|
+
return { resourceSpans: [] };
|
|
14
|
+
const session = first.session_id;
|
|
15
|
+
const traceId = hex(session, 32);
|
|
16
|
+
const spanId = (seq) => hex(`${session}:${seq}`, 16);
|
|
17
|
+
const root = spanId(0);
|
|
18
|
+
const last = events[events.length - 1];
|
|
19
|
+
const spans = [];
|
|
20
|
+
// The session itself, with the findings as its events.
|
|
21
|
+
spans.push({
|
|
22
|
+
traceId,
|
|
23
|
+
spanId: root,
|
|
24
|
+
name: "session",
|
|
25
|
+
kind: 1,
|
|
26
|
+
startTimeUnixNano: nanos(first.ts),
|
|
27
|
+
endTimeUnixNano: nanos(last.ts),
|
|
28
|
+
attributes: [
|
|
29
|
+
...str("blackbox.session_id", session),
|
|
30
|
+
...str("blackbox.mode", first.meta.mode),
|
|
31
|
+
...str("blackbox.label", first.meta.label),
|
|
32
|
+
...str("blackbox.tenant", first.meta.tenant),
|
|
33
|
+
...int("blackbox.events", events.length),
|
|
34
|
+
],
|
|
35
|
+
events: events
|
|
36
|
+
.filter((e) => e.kind === "finding")
|
|
37
|
+
.map((e) => ({
|
|
38
|
+
timeUnixNano: nanos(e.ts),
|
|
39
|
+
name: String(e.meta.code),
|
|
40
|
+
attributes: [
|
|
41
|
+
...str("blackbox.severity", e.meta.severity),
|
|
42
|
+
...str("blackbox.fault", e.meta.fault),
|
|
43
|
+
...int("blackbox.seq", e.seq),
|
|
44
|
+
],
|
|
45
|
+
})),
|
|
46
|
+
});
|
|
47
|
+
// Each model call: the request, closed by its response (request_seq) or its abnormal end.
|
|
48
|
+
const ends = new Map();
|
|
49
|
+
for (const e of events)
|
|
50
|
+
if ((e.kind === "llm.response" || e.kind === "llm.incomplete") &&
|
|
51
|
+
typeof e.meta.request_seq === "number")
|
|
52
|
+
ends.set(e.meta.request_seq, e);
|
|
53
|
+
for (const e of events.filter((x) => x.kind === "llm.request")) {
|
|
54
|
+
const end = ends.get(e.seq);
|
|
55
|
+
const usage = end?.meta.usage && typeof end.meta.usage === "object"
|
|
56
|
+
? tokensOf(end.meta.usage)
|
|
57
|
+
: null;
|
|
58
|
+
const status = end?.meta.status;
|
|
59
|
+
const failed = !end || end.kind === "llm.incomplete" || (typeof status === "number" && status >= 400);
|
|
60
|
+
const model = typeof end?.meta.model === "string" ? end.meta.model : undefined;
|
|
61
|
+
spans.push({
|
|
62
|
+
traceId,
|
|
63
|
+
spanId: spanId(e.seq),
|
|
64
|
+
parentSpanId: root,
|
|
65
|
+
name: model ? `chat ${model}` : "chat",
|
|
66
|
+
kind: 3,
|
|
67
|
+
startTimeUnixNano: nanos(e.ts),
|
|
68
|
+
endTimeUnixNano: nanos((end ?? e).ts),
|
|
69
|
+
attributes: [
|
|
70
|
+
...str("gen_ai.system", e.meta.provider),
|
|
71
|
+
...str("gen_ai.response.model", model),
|
|
72
|
+
...str("gen_ai.response.id", end?.meta.message_id ?? end?.meta.response_id),
|
|
73
|
+
...(usage
|
|
74
|
+
? [
|
|
75
|
+
...int("gen_ai.usage.input_tokens", usage.input + usage.cache_read + usage.cache_write),
|
|
76
|
+
...int("gen_ai.usage.output_tokens", usage.output),
|
|
77
|
+
]
|
|
78
|
+
: []),
|
|
79
|
+
...int("http.response.status_code", status),
|
|
80
|
+
...int("blackbox.seq", e.seq),
|
|
81
|
+
],
|
|
82
|
+
...(failed
|
|
83
|
+
? {
|
|
84
|
+
status: {
|
|
85
|
+
code: 2,
|
|
86
|
+
message: end?.kind === "llm.incomplete"
|
|
87
|
+
? String(end.meta.reason ?? "incomplete")
|
|
88
|
+
: `status ${status ?? "none"}`,
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
: {}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// Each MCP tool call, closed by its result (call_seq).
|
|
95
|
+
const results = new Map();
|
|
96
|
+
for (const e of events)
|
|
97
|
+
if (e.kind === "tool.result" && typeof e.meta.call_seq === "number")
|
|
98
|
+
results.set(e.meta.call_seq, e);
|
|
99
|
+
for (const e of events.filter((x) => x.kind === "tool.call")) {
|
|
100
|
+
const end = results.get(e.seq);
|
|
101
|
+
const failed = end?.meta.is_error === true ||
|
|
102
|
+
end?.meta.rpc_error !== undefined ||
|
|
103
|
+
e.meta.policy !== undefined;
|
|
104
|
+
spans.push({
|
|
105
|
+
traceId,
|
|
106
|
+
spanId: spanId(e.seq),
|
|
107
|
+
parentSpanId: root,
|
|
108
|
+
name: typeof e.meta.tool === "string"
|
|
109
|
+
? `execute_tool ${e.meta.tool}`
|
|
110
|
+
: `mcp ${String(e.meta.method ?? "call")}`,
|
|
111
|
+
kind: 3,
|
|
112
|
+
startTimeUnixNano: nanos(e.ts),
|
|
113
|
+
endTimeUnixNano: nanos((end ?? e).ts),
|
|
114
|
+
attributes: [
|
|
115
|
+
...str("gen_ai.tool.name", e.meta.tool),
|
|
116
|
+
...str("mcp.server", e.meta.server),
|
|
117
|
+
...str("mcp.method", e.meta.method),
|
|
118
|
+
...int("blackbox.seq", e.seq),
|
|
119
|
+
],
|
|
120
|
+
...(failed
|
|
121
|
+
? {
|
|
122
|
+
status: {
|
|
123
|
+
code: 2,
|
|
124
|
+
message: e.meta.policy !== undefined ? "denied by policy" : "tool error",
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
: {}),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
resourceSpans: [
|
|
132
|
+
{
|
|
133
|
+
resource: {
|
|
134
|
+
attributes: [
|
|
135
|
+
...str("service.name", "zanii-blackbox"),
|
|
136
|
+
...str("blackbox.session_id", session),
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
scopeSpans: [{ scope: { name: "zanii-blackbox", version: "1" }, spans }],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type DataRules } from "../data/index.ts";
|
|
2
|
+
type Obj = Record<string, unknown>;
|
|
3
|
+
export interface PackEntry {
|
|
4
|
+
pack: string;
|
|
5
|
+
status: "draft" | "reviewed";
|
|
6
|
+
notes: string;
|
|
7
|
+
/** The Arabic disclaimer, when the pack has one. */
|
|
8
|
+
notes_ar?: string;
|
|
9
|
+
framework: Obj;
|
|
10
|
+
}
|
|
11
|
+
/** spec/packs.md §1: why a pack is invalid, or undefined. */
|
|
12
|
+
export declare function checkPack(value: unknown): string | undefined;
|
|
13
|
+
/** spec/data.md §1: why an identifier is invalid, or undefined. */
|
|
14
|
+
export declare function checkIdentifier(v: unknown, at: string): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* spec/data.md §4: the data rules of packs merged in order, then the core. Pack identifiers come
|
|
17
|
+
* before the core ones; throws on an invalid pack or an identifier id in two packs.
|
|
18
|
+
*/
|
|
19
|
+
export declare function packData(packs: readonly unknown[]): DataRules;
|
|
20
|
+
/** spec/packs.md §2: the core pack, from the built-in frameworks files. */
|
|
21
|
+
export declare function corePack(compliance: {
|
|
22
|
+
notes: string;
|
|
23
|
+
frameworks: Obj;
|
|
24
|
+
}, occurrence: {
|
|
25
|
+
notes: string;
|
|
26
|
+
notes_ar?: string;
|
|
27
|
+
frameworks: Obj;
|
|
28
|
+
}): {
|
|
29
|
+
v: number;
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
status: string;
|
|
33
|
+
compliance: {
|
|
34
|
+
notes: string;
|
|
35
|
+
frameworks: Obj;
|
|
36
|
+
};
|
|
37
|
+
occurrence: {
|
|
38
|
+
notes: string;
|
|
39
|
+
notes_ar?: string;
|
|
40
|
+
frameworks: Obj;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/** spec/packs.md §2: packs merged in order; throws on an invalid pack or a repeated framework id. */
|
|
44
|
+
export declare function packFrameworks(packs: readonly unknown[]): {
|
|
45
|
+
compliance: Record<string, PackEntry>;
|
|
46
|
+
occurrence: Record<string, PackEntry>;
|
|
47
|
+
};
|
|
48
|
+
export {};
|