@zanii/blackbox 0.2.0 → 0.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/dist/a2a/index.d.ts +50 -0
- package/dist/a2a/index.js +202 -0
- package/dist/analysis/taxonomy.d.ts +18 -0
- package/dist/analysis/taxonomy.js +65 -0
- package/dist/archive/index.d.ts +39 -0
- package/dist/archive/index.js +96 -0
- package/dist/bom/index.d.ts +14 -0
- package/dist/bom/index.js +132 -0
- package/dist/compliance/art12.d.ts +35 -0
- package/dist/compliance/art12.js +163 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +8 -1
- 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 +231 -1
- package/dist/policy/index.d.ts +20 -3
- package/dist/policy/index.js +39 -4
- package/dist/session/index.d.ts +18 -0
- package/dist/session/index.js +46 -0
- package/dist/timestamp/index.d.ts +24 -0
- package/dist/timestamp/index.js +274 -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/package.json +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// EU AI Act Article 12 (record-keeping) for one session's record (spec/art12.md): what the record
|
|
2
|
+
// shows against Art. 12(1), 12(2)(a)-(c), 12(3)(a)-(d), and the six-month log retention of Art. 19(1)
|
|
3
|
+
// and 26(6). Evidence mapping from the public text of Regulation (EU) 2024/1689, not legal advice.
|
|
4
|
+
// Mirrors sdks/python/src/zanii_blackbox/art12.py.
|
|
5
|
+
/** Art. 19(1) and 26(6): "at least six months". Counted as 183 days, the longest six months. */
|
|
6
|
+
export const ART12_MIN_RETENTION_DAYS = 183;
|
|
7
|
+
const NOT_PEOPLE = new Set(["timeout", "unattended", "classifier"]);
|
|
8
|
+
const NOTES = "Evidence mapping from the public text of Regulation (EU) 2024/1689, not legal advice. Article 12 applies to high-risk AI systems; whether a system is one, and whether it complies, is for its provider, deployer and counsel to judge. One session shows one period of use, not the system's lifetime.";
|
|
9
|
+
const sorted = (s) => [...new Set(s)].sort();
|
|
10
|
+
const count = (into, key) => {
|
|
11
|
+
into[key] = (into[key] ?? 0) + 1;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Checks one session's record against Article 12. `verified`: whether its chain verifies (the caller
|
|
15
|
+
* runs the check). `retentionDays`: the deployment's retention, null when records are kept forever.
|
|
16
|
+
* `annexIII1a`: the system is a remote biometric identification system, so 12(3) applies.
|
|
17
|
+
*/
|
|
18
|
+
export function art12Check(record, options) {
|
|
19
|
+
const events = record.lines.map((l) => JSON.parse(l));
|
|
20
|
+
const annex = options.annexIII1a === true;
|
|
21
|
+
let gaps = 0;
|
|
22
|
+
let lostContact = 0;
|
|
23
|
+
let errors = 0;
|
|
24
|
+
let inputs = 0;
|
|
25
|
+
let outcome = null;
|
|
26
|
+
const findings = {};
|
|
27
|
+
const codes = [];
|
|
28
|
+
const controls = {};
|
|
29
|
+
const models = [];
|
|
30
|
+
const providers = [];
|
|
31
|
+
const sources = [];
|
|
32
|
+
const deciders = [];
|
|
33
|
+
let decisions = 0;
|
|
34
|
+
for (const e of events) {
|
|
35
|
+
const m = e.meta;
|
|
36
|
+
if (e.kind === "capture.gap")
|
|
37
|
+
gaps += typeof m.missed === "number" ? m.missed : 1;
|
|
38
|
+
else if (e.kind === "session.lost_contact")
|
|
39
|
+
lostContact++;
|
|
40
|
+
else if (e.kind === "finding") {
|
|
41
|
+
count(findings, typeof m.severity === "string" ? m.severity : "unknown");
|
|
42
|
+
if (typeof m.code === "string")
|
|
43
|
+
codes.push(m.code);
|
|
44
|
+
}
|
|
45
|
+
else if (e.kind === "control" && typeof m.action === "string") {
|
|
46
|
+
count(controls, m.action);
|
|
47
|
+
if (m.action === "approval" && typeof m.by === "string") {
|
|
48
|
+
decisions++;
|
|
49
|
+
deciders.push(m.by);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (e.kind === "llm.request") {
|
|
53
|
+
inputs++;
|
|
54
|
+
if (typeof m.provider === "string")
|
|
55
|
+
providers.push(m.provider);
|
|
56
|
+
}
|
|
57
|
+
else if (e.kind === "llm.response") {
|
|
58
|
+
if (typeof m.model === "string")
|
|
59
|
+
models.push(m.model);
|
|
60
|
+
if (typeof m.status === "number" && m.status >= 400)
|
|
61
|
+
errors++;
|
|
62
|
+
}
|
|
63
|
+
else if (e.kind === "tool.call") {
|
|
64
|
+
inputs++;
|
|
65
|
+
if (typeof m.server === "string")
|
|
66
|
+
sources.push(typeof m.tool === "string" ? `${m.server}/${m.tool}` : m.server);
|
|
67
|
+
}
|
|
68
|
+
else if (e.kind === "outcome" && typeof m.outcome === "string")
|
|
69
|
+
outcome = m.outcome;
|
|
70
|
+
}
|
|
71
|
+
const open = events.find((e) => e.kind === "session.open");
|
|
72
|
+
const close = events.find((e) => e.kind === "session.close");
|
|
73
|
+
const people = sorted(deciders.filter((d) => !NOT_PEOPLE.has(d)));
|
|
74
|
+
const retention = options.retentionDays;
|
|
75
|
+
const checks = [
|
|
76
|
+
{
|
|
77
|
+
id: "automatic-recording",
|
|
78
|
+
article: "12(1)",
|
|
79
|
+
requirement: "Events are recorded automatically over the system's lifetime: here, a record that verifies and has no capture gaps.",
|
|
80
|
+
applies: true,
|
|
81
|
+
ok: record.verified && open !== undefined && gaps === 0,
|
|
82
|
+
evidence: {
|
|
83
|
+
events: events.length,
|
|
84
|
+
verified: record.verified,
|
|
85
|
+
capture_gaps: gaps,
|
|
86
|
+
lost_contact: lostContact,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "risk-situations",
|
|
91
|
+
article: "12(2)(a)",
|
|
92
|
+
requirement: "Events relevant to identifying situations that may present a risk (Art. 79(1)) or a substantial modification.",
|
|
93
|
+
applies: true,
|
|
94
|
+
ok: null,
|
|
95
|
+
evidence: { findings, finding_codes: sorted(codes), models: sorted(models) },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "post-market-monitoring",
|
|
99
|
+
article: "12(2)(b)",
|
|
100
|
+
requirement: "Events that facilitate post-market monitoring (Art. 72).",
|
|
101
|
+
applies: true,
|
|
102
|
+
ok: null,
|
|
103
|
+
evidence: { providers: sorted(providers), models: sorted(models), errors, outcome },
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "operation-monitoring",
|
|
107
|
+
article: "12(2)(c)",
|
|
108
|
+
requirement: "Events for deployers' monitoring of the system's operation (Art. 26(5)).",
|
|
109
|
+
applies: true,
|
|
110
|
+
ok: null,
|
|
111
|
+
evidence: { controls, unattended: open?.meta.unattended === true },
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "period-of-use",
|
|
115
|
+
article: "12(3)(a)",
|
|
116
|
+
requirement: "The start and end date and time of each use.",
|
|
117
|
+
applies: annex,
|
|
118
|
+
ok: open !== undefined && close !== undefined,
|
|
119
|
+
evidence: { start: open?.ts ?? null, end: close?.ts ?? null },
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "reference-database",
|
|
123
|
+
article: "12(3)(b)",
|
|
124
|
+
requirement: "The reference database against which input data was checked.",
|
|
125
|
+
applies: annex,
|
|
126
|
+
ok: null,
|
|
127
|
+
evidence: { sources: sorted(sources) },
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "input-data",
|
|
131
|
+
article: "12(3)(c)",
|
|
132
|
+
requirement: "The input data for which the search led to a match.",
|
|
133
|
+
applies: annex,
|
|
134
|
+
ok: null,
|
|
135
|
+
evidence: { inputs },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "verifying-persons",
|
|
139
|
+
article: "12(3)(d), 14(5)",
|
|
140
|
+
requirement: "The natural persons who verified the results; for Art. 14(5), at least two, separately.",
|
|
141
|
+
applies: annex,
|
|
142
|
+
// ponytail: decisions name a role (admin, tenant), not a person; false until SSO subjects are recorded
|
|
143
|
+
ok: decisions === 0 ? null : false,
|
|
144
|
+
evidence: { decisions, deciders: people, persons_identified: false },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "log-retention",
|
|
148
|
+
article: "19(1), 26(6)",
|
|
149
|
+
requirement: "Logs are kept for at least six months, unless other Union or national law says otherwise.",
|
|
150
|
+
applies: true,
|
|
151
|
+
ok: retention === null || retention >= ART12_MIN_RETENTION_DAYS,
|
|
152
|
+
evidence: { retention_days: retention, minimum_days: ART12_MIN_RETENTION_DAYS },
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
return {
|
|
156
|
+
session_id: events[0]?.session_id ?? "",
|
|
157
|
+
regulation: "Regulation (EU) 2024/1689",
|
|
158
|
+
annex_iii_1a: annex,
|
|
159
|
+
ok: checks.every((c) => !c.applies || c.ok !== false),
|
|
160
|
+
checks,
|
|
161
|
+
notes: NOTES,
|
|
162
|
+
};
|
|
163
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
export { A2A_STREAMING, type AgentCardReport, a2aMethod, a2aRequestMeta, a2aResponseMeta, agentCardDigest, agentCardPayload, verifyAgentCard, } from "./a2a/index.ts";
|
|
1
2
|
export { causality, memoryXray } from "./agents/index.ts";
|
|
2
3
|
export { type DetectorOptions, detectors, fingerprint } from "./analysis/detectors.ts";
|
|
3
4
|
export { FAULTS, type Fault, faultOf } from "./analysis/faults.ts";
|
|
4
5
|
export { type AnalyzeOptions, analyze, type ClaimVerdict, type CriterionResult, checkFlightPlan, checkpointReplay, dualWitness, eventsOf, type Finding as AnalysisFinding, findingKey, type Landing, landing, OUTCOMES, type Outcome, type OutcomeRollup, rollup, type Severity, type WasteReason, type WasteReport, waste, } from "./analysis/index.ts";
|
|
6
|
+
export { MITRE_ATLAS, OWASP_ASI, TAXONOMY, type Taxonomy, taxonomyOf, } from "./analysis/taxonomy.ts";
|
|
5
7
|
export { type ApprovalFinding, approvalFindings } from "./approvals/index.ts";
|
|
6
8
|
export { textWarnings } from "./approvals/warnings.ts";
|
|
9
|
+
export { type ArchiveFile, type ArchiveManifest, type ArchiveReport, archiveDigest, archiveEntry, archiveManifest, verifyArchive, } from "./archive/index.ts";
|
|
7
10
|
export { type Attestation, attest, isReadOnly, normalise, parseCommand } from "./attest/index.ts";
|
|
8
11
|
export { type AuthorityFinding, authorityAt, authorityTimeline, automationSurprise, type Mode as AuthorityMode, type Span as AuthoritySpan, } from "./authority/index.ts";
|
|
9
12
|
export { type Invoice, type InvoiceLine, invoice, loadPlans, type Plan, type Plans, renderTaxInvoice, stripeSignatureOk, type TaxInvoice, type TaxInvoiceInput, taxInvoice, } from "./billing/index.ts";
|
|
13
|
+
export { type AgentIdentity, checkAgent, sessionBom } from "./bom/index.ts";
|
|
10
14
|
export { BlackboxApiError, type Client, type ClientOptions, client, type SessionQuery, } from "./client/index.ts";
|
|
15
|
+
export { ART12_MIN_RETENTION_DAYS, type Art12Check, type Art12Report, art12Check, } from "./compliance/art12.ts";
|
|
11
16
|
export { complianceFacts, complianceReport, type Facts, type Framework, type Report as ComplianceReport, renderReport, } from "./compliance/index.ts";
|
|
12
17
|
export { type CallCost, type CostReport, callMicroUsd, effectivePrice, formatAed, type LoadedPrices, loadPrices, type ModelPrice, type PriceTable, priceCall, priceFor, promptTokens, requestsOf, sessionCost, type Tokens, toAedFils, tokensOf, } from "./cost/index.ts";
|
|
13
18
|
export { type AuditRow, CORE_EVIDENCE, CORE_IDENTIFIERS, type Connector as AuditConnector, canaryIdentifier, checkAuditConnector, type DataFinding, type DataRules, dataClasses, dataFindings, dataFingerprint, dataMeta, dataScan, dataTopology, hasEvidence, type Identifier, type Lineage, lineage, luhn, normalizeValue, parseAuditLog, parseCsv, reconcileSystem, type SystemReport, type Topology, toolWitness, unevidenced, verifyToolWitness, witnessOf, } from "./data/index.ts";
|
|
@@ -28,7 +33,7 @@ export { toOtlp } from "./otlp/index.ts";
|
|
|
28
33
|
export { checkIdentifier, checkPack, corePack, type PackEntry, packData, packFrameworks, } from "./packs/index.ts";
|
|
29
34
|
export { type PolicyChange, type PolicyDelta, policyDelta } from "./policy/delta.ts";
|
|
30
35
|
export { type Draft, type DraftGroup, policyDrafts } from "./policy/drafts.ts";
|
|
31
|
-
export { audited, type CompiledPolicy, compilePolicy, type Decision, decide, loadPolicy, type Policy, policyFindings, type Rule, } from "./policy/index.ts";
|
|
36
|
+
export { audited, type CompiledPolicy, compilePolicy, type Decision, decide, loadPolicy, mcpToolHints, type Policy, policyFindings, type Rule, type ToolHints, } from "./policy/index.ts";
|
|
32
37
|
export { calibration, FEATURES, features, MIN_EACH, type PrecogModel, precogFindings, precogReport, predict, prefixOf, splitOf, TARGET, train, } from "./precog/index.ts";
|
|
33
38
|
export { type InterventionEvidence, interventionEvidence, interventionGain, } from "./precog/intervention.ts";
|
|
34
39
|
export { activities, buildNormal, type NormalModel, type NormalScore, normalScore, normalThreshold, } from "./precog/normal.ts";
|
|
@@ -39,6 +44,8 @@ export { cassette, requestKey, type Take } from "./replay/index.ts";
|
|
|
39
44
|
export { ORPHAN_TEXT, repairToolCalls } from "./replay/repair.ts";
|
|
40
45
|
export { type DrainResult, drainSpools } from "./session/drain.ts";
|
|
41
46
|
export { BlackboxSession, checkEgress, type EgressCheck, type FlightPlan, type LlmCallIds, type SessionError, type SessionOptions, type SessionState, type SessionStats, session, stableEventId, } from "./session/index.ts";
|
|
47
|
+
export { type TimestampReport, timestampRequest, verifyTimestamp, } from "./timestamp/index.ts";
|
|
48
|
+
export { addCheckpointBody, type Cbor, cborDecode, cborEncode, checkpointText, consistencyProof, cosign, cosignMldsa, ed25519PublicKey, inclusionProof, keyRotationNote, leafHash, logKid, makeReceipt, merkleRoot, mldsaAvailable, mldsaPublicKey, noteVkey, parseAddCheckpoint, parseCheckpoint, parseVkey, RECEIPT_SUB, SESSION_CONTENT_TYPE, sessionStatement, signNote, signStatement, splitNote, statementEntry, subtreeMessage, transparentStatement, verifyConsistency, verifyCoseSign1, verifyCosignature, verifyInclusion, verifyKeyRotation, verifyNote, verifyReceipt, verifyTransparency, } from "./transparency/index.ts";
|
|
42
49
|
export { type Compensation, checkCompensations, runUndo, type UndoFinding, type UndoStatus, type UndoStep, undoFindings, undoPlan, } from "./undo/index.ts";
|
|
43
50
|
export * from "./verify/index.ts";
|
|
44
51
|
export { VERSION } from "./version.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
// Public API of @zanii/blackbox. Everything customers can import is re-exported here.
|
|
2
2
|
// Keep this file in step with sdks/python/src/zanii_blackbox/__init__.py (the SDKs mirror each other).
|
|
3
|
+
export { A2A_STREAMING, a2aMethod, a2aRequestMeta, a2aResponseMeta, agentCardDigest, agentCardPayload, verifyAgentCard, } from "./a2a/index.js";
|
|
3
4
|
export { causality, memoryXray } from "./agents/index.js";
|
|
4
5
|
export { detectors, fingerprint } from "./analysis/detectors.js";
|
|
5
6
|
export { FAULTS, faultOf } from "./analysis/faults.js";
|
|
6
7
|
export { analyze, checkFlightPlan, checkpointReplay, dualWitness, eventsOf, findingKey, landing, OUTCOMES, rollup, waste, } from "./analysis/index.js";
|
|
8
|
+
export { MITRE_ATLAS, OWASP_ASI, TAXONOMY, taxonomyOf, } from "./analysis/taxonomy.js";
|
|
7
9
|
export { approvalFindings } from "./approvals/index.js";
|
|
8
10
|
export { textWarnings } from "./approvals/warnings.js";
|
|
11
|
+
export { archiveDigest, archiveEntry, archiveManifest, verifyArchive, } from "./archive/index.js";
|
|
9
12
|
export { attest, isReadOnly, normalise, parseCommand } from "./attest/index.js";
|
|
10
13
|
export { authorityAt, authorityTimeline, automationSurprise, } from "./authority/index.js";
|
|
11
14
|
export { invoice, loadPlans, renderTaxInvoice, stripeSignatureOk, taxInvoice, } from "./billing/index.js";
|
|
15
|
+
export { checkAgent, sessionBom } from "./bom/index.js";
|
|
12
16
|
export { BlackboxApiError, client, } from "./client/index.js";
|
|
17
|
+
export { ART12_MIN_RETENTION_DAYS, art12Check, } from "./compliance/art12.js";
|
|
13
18
|
export { complianceFacts, complianceReport, renderReport, } from "./compliance/index.js";
|
|
14
19
|
export { callMicroUsd, effectivePrice, formatAed, loadPrices, priceCall, priceFor, promptTokens, requestsOf, sessionCost, toAedFils, tokensOf, } from "./cost/index.js";
|
|
15
20
|
export { CORE_EVIDENCE, CORE_IDENTIFIERS, canaryIdentifier, checkAuditConnector, dataClasses, dataFindings, dataFingerprint, dataMeta, dataScan, dataTopology, hasEvidence, lineage, luhn, normalizeValue, parseAuditLog, parseCsv, reconcileSystem, toolWitness, unevidenced, verifyToolWitness, witnessOf, } from "./data/index.js";
|
|
@@ -30,7 +35,7 @@ export { toOtlp } from "./otlp/index.js";
|
|
|
30
35
|
export { checkIdentifier, checkPack, corePack, packData, packFrameworks, } from "./packs/index.js";
|
|
31
36
|
export { policyDelta } from "./policy/delta.js";
|
|
32
37
|
export { policyDrafts } from "./policy/drafts.js";
|
|
33
|
-
export { audited, compilePolicy, decide, loadPolicy, policyFindings, } from "./policy/index.js";
|
|
38
|
+
export { audited, compilePolicy, decide, loadPolicy, mcpToolHints, policyFindings, } from "./policy/index.js";
|
|
34
39
|
export { calibration, FEATURES, features, MIN_EACH, precogFindings, precogReport, predict, prefixOf, splitOf, TARGET, train, } from "./precog/index.js";
|
|
35
40
|
export { interventionEvidence, interventionGain, } from "./precog/intervention.js";
|
|
36
41
|
export { activities, buildNormal, normalScore, normalThreshold, } from "./precog/normal.js";
|
|
@@ -41,6 +46,8 @@ export { cassette, requestKey } from "./replay/index.js";
|
|
|
41
46
|
export { ORPHAN_TEXT, repairToolCalls } from "./replay/repair.js";
|
|
42
47
|
export { drainSpools } from "./session/drain.js";
|
|
43
48
|
export { BlackboxSession, checkEgress, session, stableEventId, } from "./session/index.js";
|
|
49
|
+
export { timestampRequest, verifyTimestamp, } from "./timestamp/index.js";
|
|
50
|
+
export { addCheckpointBody, cborDecode, cborEncode, checkpointText, consistencyProof, cosign, cosignMldsa, ed25519PublicKey, inclusionProof, keyRotationNote, leafHash, logKid, makeReceipt, merkleRoot, mldsaAvailable, mldsaPublicKey, noteVkey, parseAddCheckpoint, parseCheckpoint, parseVkey, RECEIPT_SUB, SESSION_CONTENT_TYPE, sessionStatement, signNote, signStatement, splitNote, statementEntry, subtreeMessage, transparentStatement, verifyConsistency, verifyCoseSign1, verifyCosignature, verifyInclusion, verifyKeyRotation, verifyNote, verifyReceipt, verifyTransparency, } from "./transparency/index.js";
|
|
44
51
|
export { checkCompensations, runUndo, undoFindings, undoPlan, } from "./undo/index.js";
|
|
45
52
|
export * from "./verify/index.js";
|
|
46
53
|
export { VERSION } from "./version.js";
|
package/dist/ocsf/index.d.ts
CHANGED
package/dist/ocsf/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// OCSF export (spec/ocsf.md, idea S1): a session's record as OCSF 1.8.0 events for a SIEM (Splunk,
|
|
2
2
|
// AWS Security Lake, CrowdStrike, Elastic), with a lossy downgrade to 1.1 / 1.3 and a one-line text
|
|
3
3
|
// form. Mirrors ocsf.py.
|
|
4
|
+
import { taxonomyOf } from "../analysis/taxonomy.js";
|
|
4
5
|
const SEVERITY = {
|
|
5
6
|
advisory: [2, "Low"],
|
|
6
7
|
caution: [3, "Medium"],
|
|
@@ -45,11 +46,16 @@ const FINDING = [2004, "Detection Finding", 2, "Findings"];
|
|
|
45
46
|
* gets what the gateway saw. */
|
|
46
47
|
export function toOcsf(lines, options = {}) {
|
|
47
48
|
const pv = options.productVersion ?? "unknown";
|
|
49
|
+
let label;
|
|
50
|
+
let session = "";
|
|
48
51
|
const requests = new Map();
|
|
49
52
|
const out = [];
|
|
50
53
|
for (const line of lines) {
|
|
51
54
|
const e = JSON.parse(line);
|
|
52
55
|
const m = e.meta;
|
|
56
|
+
session = e.session_id;
|
|
57
|
+
if (e.kind === "session.open" && typeof m.label === "string")
|
|
58
|
+
label = m.label;
|
|
53
59
|
if (e.kind === "llm.request") {
|
|
54
60
|
requests.set(e.seq, {
|
|
55
61
|
...(typeof m.provider === "string" ? { provider: m.provider } : {}),
|
|
@@ -114,9 +120,36 @@ export function toOcsf(lines, options = {}) {
|
|
|
114
120
|
out.push(ev);
|
|
115
121
|
}
|
|
116
122
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
const version = options.version ?? "1.9";
|
|
124
|
+
if (version === "1.9")
|
|
125
|
+
return out.map((ev) => upgrade(ev, label, session));
|
|
126
|
+
return version === "1.8" ? out : out.map((ev) => downgrade(ev, version));
|
|
127
|
+
}
|
|
128
|
+
/** OCSF 1.9 (spec/ocsf.md §2): the agent (`ai_agent`) on model and tool events, and each finding's
|
|
129
|
+
* MITRE ATLAS techniques (`attacks`) and OWASP ASI ids (`metadata.tags`). */
|
|
130
|
+
function upgrade(ev, label, session) {
|
|
131
|
+
const metadata = {
|
|
132
|
+
...ev.metadata,
|
|
133
|
+
version: "1.9.0",
|
|
134
|
+
};
|
|
135
|
+
const out = { ...ev, metadata };
|
|
136
|
+
const ai = ev.ai_model !== undefined ||
|
|
137
|
+
ev.api?.operation === "tools/call";
|
|
138
|
+
if (ai) {
|
|
139
|
+
out.ai_agent = { name: label ?? "agent", instance_uid: session };
|
|
140
|
+
metadata.profiles = ["ai_operation"];
|
|
141
|
+
}
|
|
142
|
+
if (ev.class_uid === 2004) {
|
|
143
|
+
const t = taxonomyOf(String(ev.finding_info.title));
|
|
144
|
+
if (t.atlas.length)
|
|
145
|
+
out.attacks = t.atlas.map((a) => ({
|
|
146
|
+
technique: { uid: a.id, name: a.name },
|
|
147
|
+
version: "ATLAS v2026.09",
|
|
148
|
+
}));
|
|
149
|
+
if (t.owasp.length)
|
|
150
|
+
metadata.tags = t.owasp.map((o) => ({ name: "owasp_asi", value: o.id }));
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
120
153
|
}
|
|
121
154
|
/** Idea S1: for a SIEM on an older OCSF: drops what came later (`ai_model`), the `ai_operation`
|
|
122
155
|
* profile, and says so in `unmapped.downgraded_from`. */
|
package/dist/otlp/index.d.ts
CHANGED
|
@@ -7,7 +7,14 @@ type Attr = {
|
|
|
7
7
|
key: string;
|
|
8
8
|
value: Value;
|
|
9
9
|
};
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* spec/otlp.md: a session as OTLP/JSON. `semconv: 2` (the default) follows the GenAI semantic
|
|
12
|
+
* conventions as of v1.42 (`gen_ai.provider.name`, `invoke_agent`, MCP `tools/call`); `semconv: 1`
|
|
13
|
+
* is the first version's output, pinned by spec/vectors/otlp.json.
|
|
14
|
+
*/
|
|
15
|
+
export declare function toOtlp(lines: readonly string[], options?: {
|
|
16
|
+
semconv?: 1 | 2;
|
|
17
|
+
}): {
|
|
11
18
|
resourceSpans: {
|
|
12
19
|
resource: {
|
|
13
20
|
attributes: Attr[];
|
package/dist/otlp/index.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
// A session as OpenTelemetry traces (spec/otlp.md): OTLP/JSON with the GenAI semantic conventions.
|
|
2
2
|
// Deterministic (ids derived from the session), so both SDKs produce the same bytes. Mirrors otlp.py.
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { taxonomyOf } from "../analysis/taxonomy.js";
|
|
4
5
|
import { tokensOf } from "../cost/index.js";
|
|
5
6
|
const hex = (s, n) => createHash("sha256").update(s).digest("hex").slice(0, n);
|
|
6
7
|
const nanos = (ts) => `${Date.parse(ts)}000000`;
|
|
7
8
|
const str = (key, v) => typeof v === "string" ? [{ key, value: { stringValue: v } }] : [];
|
|
8
9
|
const int = (key, v) => typeof v === "number" && Number.isSafeInteger(v) ? [{ key, value: { intValue: String(v) } }] : [];
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* spec/otlp.md: a session as OTLP/JSON. `semconv: 2` (the default) follows the GenAI semantic
|
|
12
|
+
* conventions as of v1.42 (`gen_ai.provider.name`, `invoke_agent`, MCP `tools/call`); `semconv: 1`
|
|
13
|
+
* is the first version's output, pinned by spec/vectors/otlp.json.
|
|
14
|
+
*/
|
|
15
|
+
export function toOtlp(lines, options = {}) {
|
|
16
|
+
return options.semconv === 1 ? toOtlpV1(lines) : toOtlpV2(lines);
|
|
17
|
+
}
|
|
18
|
+
function toOtlpV1(lines) {
|
|
10
19
|
const events = lines.map((l) => JSON.parse(l));
|
|
11
20
|
const first = events[0];
|
|
12
21
|
if (!first)
|
|
@@ -141,3 +150,224 @@ export function toOtlp(lines) {
|
|
|
141
150
|
],
|
|
142
151
|
};
|
|
143
152
|
}
|
|
153
|
+
// ---------------------------------------------------------------- semconv 2 (GenAI v1.42)
|
|
154
|
+
/** Provider kinds as `gen_ai.provider.name` well-known values; any other provider id as it is. */
|
|
155
|
+
const PROVIDER_NAMES = {
|
|
156
|
+
anthropic: "anthropic",
|
|
157
|
+
openai: "openai",
|
|
158
|
+
"azure-openai": "azure.ai.openai",
|
|
159
|
+
gemini: "gcp.gemini",
|
|
160
|
+
vertex: "gcp.vertex_ai",
|
|
161
|
+
bedrock: "aws.bedrock",
|
|
162
|
+
};
|
|
163
|
+
const list = (key, vs) => vs.length
|
|
164
|
+
? [{ key, value: { arrayValue: { values: vs.map((v) => ({ stringValue: v })) } } }]
|
|
165
|
+
: [];
|
|
166
|
+
const strs = (key, v) => typeof v === "string" ? [{ key, value: { arrayValue: { values: [{ stringValue: v }] } } }] : [];
|
|
167
|
+
const failure = (type, message) => ({
|
|
168
|
+
attrs: str("error.type", type),
|
|
169
|
+
status: { status: { code: 2, message } },
|
|
170
|
+
});
|
|
171
|
+
function toOtlpV2(lines) {
|
|
172
|
+
const events = lines.map((l) => JSON.parse(l));
|
|
173
|
+
const first = events[0];
|
|
174
|
+
if (!first)
|
|
175
|
+
return { resourceSpans: [] };
|
|
176
|
+
const session = first.session_id;
|
|
177
|
+
const traceId = hex(session, 32);
|
|
178
|
+
const spanId = (seq) => hex(`${session}:${seq}`, 16);
|
|
179
|
+
const root = spanId(0);
|
|
180
|
+
const last = events[events.length - 1];
|
|
181
|
+
const label = typeof first.meta.label === "string" ? first.meta.label : undefined;
|
|
182
|
+
const conversation = str("gen_ai.conversation.id", session);
|
|
183
|
+
const spans = [];
|
|
184
|
+
// The session is the agent's invocation, with the findings as its events.
|
|
185
|
+
spans.push({
|
|
186
|
+
traceId,
|
|
187
|
+
spanId: root,
|
|
188
|
+
name: label ? `invoke_agent ${label}` : "invoke_agent",
|
|
189
|
+
kind: 1,
|
|
190
|
+
startTimeUnixNano: nanos(first.ts),
|
|
191
|
+
endTimeUnixNano: nanos(last.ts),
|
|
192
|
+
attributes: [
|
|
193
|
+
...str("gen_ai.operation.name", "invoke_agent"),
|
|
194
|
+
...str("gen_ai.agent.name", label),
|
|
195
|
+
...conversation,
|
|
196
|
+
...str("blackbox.session_id", session),
|
|
197
|
+
...str("blackbox.mode", first.meta.mode),
|
|
198
|
+
...str("blackbox.tenant", first.meta.tenant),
|
|
199
|
+
...int("blackbox.events", events.length),
|
|
200
|
+
],
|
|
201
|
+
events: events
|
|
202
|
+
.filter((e) => e.kind === "finding")
|
|
203
|
+
.map((e) => ({
|
|
204
|
+
timeUnixNano: nanos(e.ts),
|
|
205
|
+
name: String(e.meta.code),
|
|
206
|
+
attributes: [
|
|
207
|
+
...str("blackbox.severity", e.meta.severity),
|
|
208
|
+
...str("blackbox.fault", e.meta.fault),
|
|
209
|
+
...list("blackbox.owasp_asi", taxonomyOf(String(e.meta.code)).owasp.map((o) => o.id)),
|
|
210
|
+
...list("blackbox.mitre_atlas", taxonomyOf(String(e.meta.code)).atlas.map((a) => a.id)),
|
|
211
|
+
...int("blackbox.seq", e.seq),
|
|
212
|
+
],
|
|
213
|
+
})),
|
|
214
|
+
});
|
|
215
|
+
// Model calls: CLIENT spans named `chat {model}`.
|
|
216
|
+
const ends = new Map();
|
|
217
|
+
for (const e of events)
|
|
218
|
+
if ((e.kind === "llm.response" || e.kind === "llm.incomplete") &&
|
|
219
|
+
typeof e.meta.request_seq === "number")
|
|
220
|
+
ends.set(e.meta.request_seq, e);
|
|
221
|
+
for (const e of events.filter((x) => x.kind === "llm.request")) {
|
|
222
|
+
const end = ends.get(e.seq);
|
|
223
|
+
const usage = end?.meta.usage && typeof end.meta.usage === "object"
|
|
224
|
+
? tokensOf(end.meta.usage)
|
|
225
|
+
: null;
|
|
226
|
+
const status = end?.meta.status;
|
|
227
|
+
const model = typeof end?.meta.model === "string" ? end.meta.model : undefined;
|
|
228
|
+
const provider = typeof e.meta.provider === "string" ? e.meta.provider : undefined;
|
|
229
|
+
const reason = String(end?.meta.reason ?? "incomplete");
|
|
230
|
+
const fail = !end
|
|
231
|
+
? failure("incomplete", "no response recorded")
|
|
232
|
+
: end.kind === "llm.incomplete"
|
|
233
|
+
? failure(reason, reason)
|
|
234
|
+
: typeof status === "number" && status >= 400
|
|
235
|
+
? failure(String(status), `status ${status}`)
|
|
236
|
+
: null;
|
|
237
|
+
spans.push({
|
|
238
|
+
traceId,
|
|
239
|
+
spanId: spanId(e.seq),
|
|
240
|
+
parentSpanId: root,
|
|
241
|
+
name: model ? `chat ${model}` : "chat",
|
|
242
|
+
kind: 3,
|
|
243
|
+
startTimeUnixNano: nanos(e.ts),
|
|
244
|
+
endTimeUnixNano: nanos((end ?? e).ts),
|
|
245
|
+
attributes: [
|
|
246
|
+
...str("gen_ai.operation.name", "chat"),
|
|
247
|
+
...str("gen_ai.provider.name", provider ? (PROVIDER_NAMES[provider] ?? provider) : undefined),
|
|
248
|
+
...str("gen_ai.response.model", model),
|
|
249
|
+
...str("gen_ai.response.id", end?.meta.message_id ?? end?.meta.response_id),
|
|
250
|
+
...strs("gen_ai.response.finish_reasons", end?.meta.stop_reason ?? end?.meta.finish_reason),
|
|
251
|
+
...(usage
|
|
252
|
+
? [
|
|
253
|
+
...int("gen_ai.usage.input_tokens", usage.input + usage.cache_read + usage.cache_write),
|
|
254
|
+
...int("gen_ai.usage.output_tokens", usage.output),
|
|
255
|
+
...(usage.cache_read
|
|
256
|
+
? int("gen_ai.usage.cache_read.input_tokens", usage.cache_read)
|
|
257
|
+
: []),
|
|
258
|
+
...(usage.cache_write
|
|
259
|
+
? int("gen_ai.usage.cache_write.input_tokens", usage.cache_write)
|
|
260
|
+
: []),
|
|
261
|
+
]
|
|
262
|
+
: []),
|
|
263
|
+
...conversation,
|
|
264
|
+
...str("blackbox.provider", provider),
|
|
265
|
+
...str("blackbox.region", e.meta.region),
|
|
266
|
+
...int("http.response.status_code", status),
|
|
267
|
+
...(fail ? fail.attrs : []),
|
|
268
|
+
...int("blackbox.seq", e.seq),
|
|
269
|
+
],
|
|
270
|
+
...(fail ? fail.status : {}),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
// MCP and broker calls, closed by their result (call_seq). MCP spans follow the MCP conventions.
|
|
274
|
+
const results = new Map();
|
|
275
|
+
for (const e of events)
|
|
276
|
+
if (e.kind === "tool.result" && typeof e.meta.call_seq === "number")
|
|
277
|
+
results.set(e.meta.call_seq, e);
|
|
278
|
+
for (const e of events.filter((x) => x.kind === "tool.call")) {
|
|
279
|
+
const end = results.get(e.seq);
|
|
280
|
+
const broker = e.meta.via === "broker";
|
|
281
|
+
const method = typeof e.meta.method === "string" ? e.meta.method : undefined;
|
|
282
|
+
const tool = typeof e.meta.tool === "string" ? e.meta.tool : undefined;
|
|
283
|
+
const isCall = broker || method === "tools/call";
|
|
284
|
+
const fail = e.meta.policy !== undefined
|
|
285
|
+
? failure("policy_denied", "denied by policy")
|
|
286
|
+
: end?.meta.rpc_error !== undefined
|
|
287
|
+
? failure(String(end.meta.rpc_error), "rpc error")
|
|
288
|
+
: end?.meta.is_error === true
|
|
289
|
+
? failure("tool_error", "tool error")
|
|
290
|
+
: broker && typeof end?.meta.status === "number" && end.meta.status >= 400
|
|
291
|
+
? failure(String(end.meta.status), `status ${end.meta.status}`)
|
|
292
|
+
: null;
|
|
293
|
+
const rpcId = e.meta.rpc_id;
|
|
294
|
+
spans.push({
|
|
295
|
+
traceId,
|
|
296
|
+
spanId: spanId(e.seq),
|
|
297
|
+
parentSpanId: root,
|
|
298
|
+
name: broker
|
|
299
|
+
? `execute_tool ${tool ?? "api"}`
|
|
300
|
+
: `${method ?? "mcp"}${tool && isCall ? ` ${tool}` : ""}`,
|
|
301
|
+
kind: 3,
|
|
302
|
+
startTimeUnixNano: nanos(e.ts),
|
|
303
|
+
endTimeUnixNano: nanos((end ?? e).ts),
|
|
304
|
+
attributes: [
|
|
305
|
+
...(isCall ? str("gen_ai.operation.name", "execute_tool") : []),
|
|
306
|
+
...str("gen_ai.tool.name", tool),
|
|
307
|
+
...(broker ? str("gen_ai.tool.type", "extension") : []),
|
|
308
|
+
...(broker ? [] : str("mcp.method.name", method)),
|
|
309
|
+
...(broker ? [] : str("mcp.protocol.version", e.meta.protocol_version)),
|
|
310
|
+
...(typeof rpcId === "string" || typeof rpcId === "number"
|
|
311
|
+
? str("jsonrpc.request.id", String(rpcId))
|
|
312
|
+
: []),
|
|
313
|
+
...(broker ? str("http.request.method", e.meta.method) : []),
|
|
314
|
+
...(broker ? str("url.path", e.meta.path) : []),
|
|
315
|
+
...str("blackbox.mcp_server", e.meta.server),
|
|
316
|
+
...int("rpc.response.status_code", end?.meta.rpc_error),
|
|
317
|
+
...conversation,
|
|
318
|
+
...(fail ? fail.attrs : []),
|
|
319
|
+
...int("blackbox.seq", e.seq),
|
|
320
|
+
],
|
|
321
|
+
...(fail ? fail.status : {}),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
// The agent's own tools (SDK events): INTERNAL spans, from a call to the next result of its name.
|
|
325
|
+
const open = new Map();
|
|
326
|
+
const sdk = (e, type) => e.kind === "sdk.event" && e.meta.type === type;
|
|
327
|
+
for (const e of events) {
|
|
328
|
+
const name = typeof e.meta.name === "string" ? e.meta.name : undefined;
|
|
329
|
+
if (!name)
|
|
330
|
+
continue;
|
|
331
|
+
if (sdk(e, "tool.call"))
|
|
332
|
+
open.set(name, [...(open.get(name) ?? []), e]);
|
|
333
|
+
if (!sdk(e, "tool.result"))
|
|
334
|
+
continue;
|
|
335
|
+
const call = open.get(name)?.shift();
|
|
336
|
+
spans.push({
|
|
337
|
+
traceId,
|
|
338
|
+
spanId: spanId(e.seq),
|
|
339
|
+
parentSpanId: root,
|
|
340
|
+
name: `execute_tool ${name}`,
|
|
341
|
+
kind: 1,
|
|
342
|
+
startTimeUnixNano: nanos((call ?? e).ts),
|
|
343
|
+
endTimeUnixNano: nanos(e.ts),
|
|
344
|
+
attributes: [
|
|
345
|
+
...str("gen_ai.operation.name", "execute_tool"),
|
|
346
|
+
...str("gen_ai.tool.name", name),
|
|
347
|
+
...str("gen_ai.tool.type", "function"),
|
|
348
|
+
...conversation,
|
|
349
|
+
...str("blackbox.reported_by", "agent"),
|
|
350
|
+
...int("blackbox.seq", e.seq),
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
resourceSpans: [
|
|
356
|
+
{
|
|
357
|
+
resource: {
|
|
358
|
+
attributes: [
|
|
359
|
+
...str("service.name", "zanii-blackbox"),
|
|
360
|
+
...str("blackbox.session_id", session),
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
scopeSpans: [
|
|
364
|
+
{
|
|
365
|
+
scope: { name: "zanii-blackbox", version: "2" },
|
|
366
|
+
schemaUrl: "https://opentelemetry.io/schemas/1.44.0",
|
|
367
|
+
spans,
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
};
|
|
373
|
+
}
|
package/dist/policy/index.d.ts
CHANGED
|
@@ -11,7 +11,23 @@ export interface Rule {
|
|
|
11
11
|
reason?: string;
|
|
12
12
|
/** N3 (idea C5): record-only. Never decides a call; what it would do is recorded (POLICY_AUDIT). */
|
|
13
13
|
audit?: boolean;
|
|
14
|
+
/** spec/policy.md §1: MCP tool annotations the call must have (from the server's `tools/list`). */
|
|
15
|
+
hints?: Partial<ToolHints>;
|
|
14
16
|
}
|
|
17
|
+
/** MCP tool annotations as booleans, with the spec's defaults for what a server leaves out. */
|
|
18
|
+
export interface ToolHints {
|
|
19
|
+
read_only: boolean;
|
|
20
|
+
destructive: boolean;
|
|
21
|
+
idempotent: boolean;
|
|
22
|
+
open_world: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* spec/policy.md §1: a tool's annotations (MCP `readOnlyHint`, `destructiveHint`, `idempotentHint`,
|
|
26
|
+
* `openWorldHint`) as hints. Missing ones take MCP's defaults: not read-only, destructive, not
|
|
27
|
+
* idempotent, open-world; destructive and idempotent only mean something for a tool that writes.
|
|
28
|
+
* An unknown tool gets the defaults too. Hints are the server's word, not proof.
|
|
29
|
+
*/
|
|
30
|
+
export declare function mcpToolHints(annotations?: unknown): ToolHints;
|
|
15
31
|
export interface Policy {
|
|
16
32
|
version: 1;
|
|
17
33
|
rules: Rule[];
|
|
@@ -31,11 +47,12 @@ interface Compiled {
|
|
|
31
47
|
/** Parses a policy file; throws on anything invalid (the server refuses to start). */
|
|
32
48
|
export declare function loadPolicy(bytes: Uint8Array): Policy;
|
|
33
49
|
export declare function compilePolicy(policy: Policy): Compiled[];
|
|
34
|
-
/** The first rule that matches this call, or null (allowed). `names`: the tool's names (an MCP tool
|
|
35
|
-
|
|
50
|
+
/** The first rule that matches this call, or null (allowed). `names`: the tool's names (an MCP tool
|
|
51
|
+
* has two). `hints`: an MCP tool's annotations; a rule with `hints` never matches a call without. */
|
|
52
|
+
export declare function decide(compiled: readonly Compiled[], names: readonly string[], args: unknown, hints?: ToolHints): Decision | null;
|
|
36
53
|
export type CompiledPolicy = ReturnType<typeof compilePolicy>;
|
|
37
54
|
/** N3 (idea C5): every audit rule this call matches, and what it would do if it were enforced. */
|
|
38
|
-
export declare function audited(compiled: readonly Compiled[], names: readonly string[], args: unknown): Array<{
|
|
55
|
+
export declare function audited(compiled: readonly Compiled[], names: readonly string[], args: unknown, hints?: ToolHints): Array<{
|
|
39
56
|
rule: string;
|
|
40
57
|
would: Rule["action"];
|
|
41
58
|
}>;
|