@shieldaai/agent-map 0.0.0-stage → 0.1.0-alpha.2

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.
@@ -0,0 +1,394 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { immutableJson } from '@shieldaai/agent-guard';
3
+ import { MapQueryError } from './types.js';
4
+ const MAPPING = Object.freeze({
5
+ 's3:GetObject': 'CAN_READ', 's3:PutObject': 'CAN_WRITE', 's3:ListBucket': 'CAN_LIST',
6
+ });
7
+ const VERSION = 's3-identity-policy-v1';
8
+ const HASH = /^[a-f0-9]{64}$/;
9
+ const reason = (record, status, code, observedAt = null) => ({ ...record, status, reason: code, observedAt, candidates: [] });
10
+ function value(node, name) { return typeof node.properties[name] === 'string' ? node.properties[name] : null; }
11
+ function fail(code = 'MAP_UNAVAILABLE') { throw new MapQueryError(code); }
12
+ /** Pair only the selected task's grants with its deployment's declared execution identity. */
13
+ export function policyPlan(companyId, tenantId, nodes, edges, maximum) {
14
+ const byId = new Map(nodes.map(node => [node.id, node]));
15
+ const outgoing = new Map(), incoming = new Map();
16
+ for (const edge of edges) {
17
+ for (const [index, from, to] of [[outgoing, edge.source, edge.target], [incoming, edge.target, edge.source]]) {
18
+ let kinds = index.get(from);
19
+ if (!kinds)
20
+ index.set(from, kinds = new Map());
21
+ let ids = kinds.get(edge.kind);
22
+ if (!ids)
23
+ kinds.set(edge.kind, ids = []);
24
+ ids.push(to);
25
+ }
26
+ }
27
+ let work = 0;
28
+ const step = (n = 1) => { work += n; if (work > 100_000)
29
+ fail('MAP_LIMIT_EXCEEDED'); };
30
+ const linked = (node, kind, label, reverse = false) => {
31
+ const ids = (reverse ? incoming : outgoing).get(node.id)?.get(kind) ?? [];
32
+ step(ids.length);
33
+ return ids.flatMap(id => { const item = byId.get(id); return item?.label === label ? [item] : []; });
34
+ };
35
+ const exact = (node, kind, target) => {
36
+ const ids = outgoing.get(node.id)?.get(kind) ?? [];
37
+ step();
38
+ return ids.length === 1 && ids[0] === target.id;
39
+ };
40
+ const records = [], probes = [], seen = new Set();
41
+ for (const task of nodes.filter(node => node.label === 'GuardTask')) {
42
+ const deployments = linked(task, 'G_ASSIGNED_TO', 'GuardDeployment');
43
+ for (const deployment of deployments) {
44
+ const agents = linked(deployment, 'G_INSTANCE_OF', 'GuardAgent');
45
+ const executions = linked(deployment, 'G_EXECUTES_FOR', 'GuardExecutionBinding', true);
46
+ for (const grant of linked(task, 'G_HAS_GRANT', 'GuardGrant')) {
47
+ const tool = value(grant, 'tool');
48
+ if (!tool)
49
+ continue;
50
+ for (const selector of linked(grant, 'G_TARGETS', 'GuardResource')) {
51
+ for (const binding of linked(selector, 'G_BINDS_SELECTOR', 'GuardResourceBinding', true)) {
52
+ step();
53
+ if (value(binding, 'tool') !== tool || value(selector, 'tool') !== tool)
54
+ continue;
55
+ const selectorValue = value(selector, 'resource_id');
56
+ if (selectorValue !== value(binding, 'resource_ref'))
57
+ continue;
58
+ const grantResources = grant.properties.resources;
59
+ if (!Array.isArray(grantResources) || !grantResources.includes(selectorValue))
60
+ continue;
61
+ const actions = binding.properties.actions;
62
+ if (!Array.isArray(actions) || actions.length > 128 || actions.some(action => typeof action !== 'string' || !action || action.length > 4096))
63
+ fail('MAP_UNAVAILABLE');
64
+ for (const execution of executions.filter(item => value(item, 'tool') === tool)) {
65
+ const identities = linked(execution, 'G_USES_IDENTITY', 'GuardExternalIdentity');
66
+ const resources = linked(binding, 'G_MAPS_TO', 'GuardExternalResource');
67
+ const executionDeployments = linked(execution, 'G_EXECUTES_FOR', 'GuardDeployment');
68
+ const selectors = linked(binding, 'G_BINDS_SELECTOR', 'GuardResource');
69
+ const grantTools = linked(grant, 'G_FOR_TOOL', 'GuardTool');
70
+ const executionTools = linked(execution, 'G_EXECUTES_TOOL', 'GuardTool');
71
+ const unambiguous = deployments.length === 1 && executionDeployments.length === 1 &&
72
+ agents.length === 1 && exact(deployment, 'G_INSTANCE_OF', agents[0]) &&
73
+ executionDeployments[0].id === deployment.id && selectors.length === 1 && selectors[0].id === selector.id &&
74
+ grantTools.length === 1 && executionTools.length === 1 && grantTools[0].id === executionTools[0].id &&
75
+ value(grantTools[0], 'name') === tool && exact(task, 'G_ASSIGNED_TO', deployment) &&
76
+ exact(execution, 'G_EXECUTES_FOR', deployment) && exact(binding, 'G_BINDS_SELECTOR', selector) &&
77
+ exact(grant, 'G_FOR_TOOL', grantTools[0]) && exact(execution, 'G_EXECUTES_TOOL', executionTools[0]);
78
+ for (const action of actions) {
79
+ step();
80
+ const key = JSON.stringify([task.id, deployment.id, execution.id, binding.id, action]);
81
+ if (seen.has(key))
82
+ continue;
83
+ seen.add(key);
84
+ if (records.length >= maximum)
85
+ fail('MAP_LIMIT_EXCEEDED');
86
+ const identity = identities.length === 1 ? identities[0] : null;
87
+ const resource = resources.length === 1 ? resources[0] : null;
88
+ const base = { taskId: task.entityId, deploymentId: deployment.entityId,
89
+ executionBindingId: execution.entityId, resourceBindingId: binding.entityId,
90
+ identityId: identity?.entityId ?? null, resourceId: resource?.entityId ?? null,
91
+ action, status: 'not_collected', reason: 'collection_missing', observedAt: null, assessment: 'unknown', candidates: [] };
92
+ const recordIndex = records.length;
93
+ const relationship = MAPPING[action];
94
+ if (!relationship) {
95
+ records.push(reason(base, 'not_collected', 'unsupported_action'));
96
+ continue;
97
+ }
98
+ if (value(execution, 'identity_selection') !== 'static') {
99
+ records.push(reason(base, 'not_collected', 'identity_not_static'));
100
+ continue;
101
+ }
102
+ if (!unambiguous || !identity || !resource || !exact(execution, 'G_USES_IDENTITY', identity) || !exact(binding, 'G_MAPS_TO', resource)) {
103
+ records.push(reason(base, 'unavailable', 'binding_ambiguous'));
104
+ continue;
105
+ }
106
+ const accountId = value(identity, 'account_id'), partition = value(identity, 'partition');
107
+ const principalArn = value(identity, 'arn'), resourceArn = value(resource, 'arn');
108
+ const principal = /^arn:aws:iam::([0-9]{12}):(role|user)\/.+$/.exec(principalArn ?? '');
109
+ const bucket = /^arn:aws:s3:::([a-z0-9][a-z0-9.-]{1,61}[a-z0-9])(?:\/(.+))?$/.exec(resourceArn ?? '');
110
+ const kind = value(resource, 'kind');
111
+ if (value(identity, 'provider') !== 'aws' || value(resource, 'provider') !== 'aws' || partition !== 'aws' ||
112
+ value(resource, 'partition') !== partition || !accountId || !/^[0-9]{12}$/.test(accountId) ||
113
+ value(resource, 'account_id') !== accountId || !principal || principal[1] !== accountId || !bucket ||
114
+ !['bucket', 'object', 'prefix'].includes(kind ?? '') || (kind === 'bucket') !== (bucket[2] === undefined)) {
115
+ records.push(reason(base, 'not_collected', 'unsupported_binding'));
116
+ continue;
117
+ }
118
+ if (identity.properties.cloud_status === null || identity.properties.cloud_status === undefined ||
119
+ resource.properties.cloud_status === null || resource.properties.cloud_status === undefined) {
120
+ records.push(reason(base, 'not_collected', 'cloud_not_collected'));
121
+ continue;
122
+ }
123
+ if (identity.properties.cloud_status !== 'resolved' || resource.properties.cloud_status !== 'resolved') {
124
+ records.push(reason(base, 'unavailable', 'cloud_unresolved'));
125
+ continue;
126
+ }
127
+ // Python's scope ID uses compact ensure_ascii JSON; preserve that byte contract.
128
+ const encoded = JSON.stringify([companyId, tenantId, partition, accountId]).replace(/[\u007f-\uffff]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
129
+ records.push(base);
130
+ probes.push({ key: recordIndex, identity, resource, accountId, partition, principalArn: principalArn,
131
+ bucketArn: `arn:aws:s3:::${bucket[1]}`, bucketName: bucket[1], principalLabel: principal[2] === 'role' ? 'AWSRole' : 'AWSUser',
132
+ relationship, collectionId: createHash('sha256').update(encoded).digest('hex'), proof: {
133
+ agentNodeId: agents[0].id, agentEntityId: agents[0].entityId, agentTeamId: value(agents[0], 'team_id'),
134
+ taskNodeId: task.id, taskEntityId: task.entityId, deploymentNodeId: deployment.id, deploymentEntityId: deployment.entityId,
135
+ executionNodeId: execution.id, executionEntityId: execution.entityId, grantNodeId: grant.id, grantEntityId: grant.entityId,
136
+ selectorNodeId: selector.id, selectorEntityId: selector.entityId, bindingNodeId: binding.id, bindingEntityId: binding.entityId,
137
+ toolNodeId: grantTools[0].id, toolEntityId: grantTools[0].entityId, tool, resourceRef: selectorValue, action,
138
+ } });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ return { records, probes };
147
+ }
148
+ // These are fixed property projections; neither query text nor property names come from requests.
149
+ const referenceText = { id: 4096, entity_id: 4096, provider: 32, partition: 32, account_id: 12, region: 64, arn: 2048,
150
+ kind: 32, cloud_status: 32, cloud_assessment: 32, cloud_provider_label: 32, cloud_provider_id: 2048, cloud_match_kind: 32 };
151
+ const referenceNumbers = ['cloud_observed_at', 'cloud_node_updated', 'cloud_ownership_updated', 'cloud_account_updated'];
152
+ const referenceFields = [...Object.keys(referenceText), ...referenceNumbers];
153
+ const referenceProjection = (variable) => [
154
+ ...Object.entries(referenceText).map(([field, maximum]) => `${field}:substring(${variable}.${field},0,${maximum + 1})`),
155
+ ...referenceNumbers.map(field => `${field}:${variable}.${field}`),
156
+ ].join(',');
157
+ const memberFields = 'eid:elementId(m),capture_hash:substring(m.capture_hash,0,65),captured_at:m.captured_at,region:substring(m.region,0,65),policy_status:substring(m.policy_status,0,33),policy_boundary_present:m.policy_boundary_present,policy_hash:substring(m.policy_hash,0,65),policy_mapping_version:substring(m.policy_mapping_version,0,65),policy_assessment:substring(m.policy_assessment,0,33)';
158
+ const proofScope = (variable) => `${variable}.source_id=$sourceId AND ${variable}.projection_id=$projectionId AND ${variable}.tenant_id=$tenantId`;
159
+ // Correlate short paths instead of planning one large cycle. Each nested branch
160
+ // retains the same bound endpoints; NOT EXISTS still rejects a second relation.
161
+ // Reduce cold planning cost without relaxing the existing query deadline or
162
+ // removing scope, binding identity or ambiguity checks.
163
+ const bindingProof = `EXISTS {
164
+ MATCH (task:GuardTask {id:pair.taskNodeId,entity_id:pair.taskEntityId})-[td:G_ASSIGNED_TO]->(deployment:GuardDeployment {id:pair.deploymentNodeId,entity_id:pair.deploymentEntityId})
165
+ WHERE ${proofScope('task')} AND ${proofScope('deployment')}
166
+ AND NOT EXISTS { MATCH (task)-[other:G_ASSIGNED_TO]->() WHERE other<>td }
167
+ AND EXISTS {
168
+ MATCH (deployment)-[da:G_INSTANCE_OF]->(agent:GuardAgent {id:pair.agentNodeId,entity_id:pair.agentEntityId})
169
+ WHERE ${proofScope('agent')} AND ($allTeams OR agent.team_id IN $teamIds)
170
+ AND (agent.team_id=pair.agentTeamId OR (agent.team_id IS NULL AND pair.agentTeamId IS NULL))
171
+ AND NOT EXISTS { MATCH (deployment)-[other:G_INSTANCE_OF]->() WHERE other<>da }
172
+ }
173
+ AND EXISTS {
174
+ MATCH (execution:GuardExecutionBinding {id:pair.executionNodeId,entity_id:pair.executionEntityId})-[ed:G_EXECUTES_FOR]->(deployment)
175
+ WHERE ${proofScope('execution')} AND execution.tool=pair.tool AND execution.identity_selection='static'
176
+ AND NOT EXISTS { MATCH (execution)-[other:G_EXECUTES_FOR]->() WHERE other<>ed }
177
+ AND EXISTS {
178
+ MATCH (execution)-[ei:G_USES_IDENTITY]->(i)
179
+ WHERE NOT EXISTS { MATCH (execution)-[other:G_USES_IDENTITY]->() WHERE other<>ei }
180
+ }
181
+ AND EXISTS {
182
+ MATCH (execution)-[et:G_EXECUTES_TOOL]->(tool:GuardTool {id:pair.toolNodeId,entity_id:pair.toolEntityId})
183
+ WHERE ${proofScope('tool')} AND tool.name=pair.tool
184
+ AND NOT EXISTS { MATCH (execution)-[other:G_EXECUTES_TOOL]->() WHERE other<>et }
185
+ AND EXISTS {
186
+ MATCH (task)-[tg:G_HAS_GRANT]->(grant:GuardGrant {id:pair.grantNodeId,entity_id:pair.grantEntityId})-[gt:G_FOR_TOOL]->(tool)
187
+ WHERE ${proofScope('grant')} AND grant.tool=pair.tool AND pair.resourceRef IN grant.resources
188
+ AND NOT EXISTS { MATCH (grant)-[other:G_FOR_TOOL]->() WHERE other<>gt }
189
+ AND NOT EXISTS { MATCH (task)-[other:G_HAS_GRANT]->(grant) WHERE other<>tg }
190
+ AND EXISTS {
191
+ MATCH (grant)-[gs:G_TARGETS]->(selector:GuardResource {id:pair.selectorNodeId,entity_id:pair.selectorEntityId})
192
+ <-[bs:G_BINDS_SELECTOR]-(binding:GuardResourceBinding {id:pair.bindingNodeId,entity_id:pair.bindingEntityId})-[bx:G_MAPS_TO]->(x)
193
+ WHERE ${proofScope('selector')} AND ${proofScope('binding')}
194
+ AND binding.tool=pair.tool AND binding.resource_ref=pair.resourceRef AND pair.action IN binding.actions
195
+ AND selector.tool=pair.tool AND selector.resource_id=pair.resourceRef
196
+ AND NOT EXISTS { MATCH (binding)-[other:G_BINDS_SELECTOR]->() WHERE other<>bs }
197
+ AND NOT EXISTS { MATCH (binding)-[other:G_MAPS_TO]->() WHERE other<>bx }
198
+ AND NOT EXISTS { MATCH (grant)-[other:G_TARGETS]->(selector) WHERE other<>gs }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }`;
204
+ export const POLICY_QUERY = `UNWIND $policyPairs AS pair
205
+ OPTIONAL MATCH (i:GuardExternalIdentity {id:pair.identityNodeId,source_id:$sourceId,projection_id:$projectionId,tenant_id:$tenantId})
206
+ OPTIONAL MATCH (x:GuardExternalResource {id:pair.resourceNodeId,source_id:$sourceId,projection_id:$projectionId,tenant_id:$tenantId})
207
+ OPTIONAL MATCH (i)-[ic:G_CLOUD_MATCH]->(p)
208
+ OPTIONAL MATCH (x)-[bc:G_CLOUD_MATCH|G_CLOUD_CONTAINER]->(b)
209
+ OPTIONAL MATCH (pa:AWSAccount)-[po:RESOURCE]->(p)
210
+ OPTIONAL MATCH (ba:AWSAccount)-[bo:RESOURCE]->(b)
211
+ OPTIONAL MATCH (c:GuardAwsCollection {company_id:$companyId,tenant_id:$tenantId,partition:pair.partition,account_id:pair.accountId})
212
+ OPTIONAL MATCH (c)-[pm:G_AWS_OBSERVED]->(p)
213
+ OPTIONAL MATCH (c)-[bm:G_AWS_OBSERVED]->(b)
214
+ OPTIONAL MATCH (p)-[candidate:G_AWS_POLICY_CANDIDATE {collection_id:c.id}]->(b)
215
+ RETURN pair.key AS key,${bindingProof} AS bindingProof,
216
+ i {${referenceProjection('i')}} AS identity,x {${referenceProjection('x')}} AS resource,
217
+ CASE WHEN ic IS NULL THEN null ELSE {eid:elementId(ic),kind:type(ic),evidence_kind:substring(ic.evidence_kind,0,65),assessment:substring(ic.assessment,0,33),observed_at:ic.observed_at} END AS identityLink,
218
+ CASE WHEN bc IS NULL THEN null ELSE {eid:elementId(bc),kind:type(bc),evidence_kind:substring(bc.evidence_kind,0,65),assessment:substring(bc.assessment,0,33),observed_at:bc.observed_at} END AS resourceLink,
219
+ CASE WHEN p IS NULL THEN null ELSE {eid:elementId(p),id:substring(p.id,0,2049),arn:substring(p.arn,0,2049),lastupdated:p.lastupdated,principal:p:AWSPrincipal,role:p:AWSRole,user:p:AWSUser} END AS principal,
220
+ CASE WHEN b IS NULL THEN null ELSE {eid:elementId(b),id:substring(b.id,0,64),arn:substring(b.arn,0,2049),region:substring(b.region,0,65),lastupdated:b.lastupdated,bucket:b:AWSS3Bucket} END AS bucket,
221
+ CASE WHEN pa IS NULL THEN null ELSE {eid:elementId(pa),id:substring(pa.id,0,13),lastupdated:pa.lastupdated} END AS principalAccount,
222
+ CASE WHEN ba IS NULL THEN null ELSE {eid:elementId(ba),id:substring(ba.id,0,13),lastupdated:ba.lastupdated} END AS bucketAccount,
223
+ CASE WHEN po IS NULL THEN null ELSE {eid:elementId(po),lastupdated:po.lastupdated} END AS principalOwnership,
224
+ CASE WHEN bo IS NULL THEN null ELSE {eid:elementId(bo),lastupdated:bo.lastupdated} END AS bucketOwnership,
225
+ CASE WHEN c IS NULL THEN null ELSE {eid:elementId(c),id:substring(c.id,0,65),schema_version:c.schema_version,status:substring(c.status,0,33),captured_at:c.captured_at,content_hash:substring(c.content_hash,0,65),policy_status:substring(c.policy_status,0,33),policy_hash:substring(c.policy_hash,0,65),policy_mapping_version:substring(c.policy_mapping_version,0,65)} END AS collection,
226
+ CASE WHEN pm IS NULL THEN null ELSE {${memberFields.replaceAll('m.', 'pm.').replaceAll('elementId(m)', 'elementId(pm)')}} END AS principalMember,
227
+ CASE WHEN bm IS NULL THEN null ELSE {${memberFields.replaceAll('m.', 'bm.').replaceAll('elementId(m)', 'elementId(bm)')}} END AS bucketMember,
228
+ CASE WHEN candidate IS NULL THEN null ELSE {eid:elementId(candidate),collection_id:substring(candidate.collection_id,0,65),capture_hash:substring(candidate.capture_hash,0,65),captured_at:candidate.captured_at,policy_hash:substring(candidate.policy_hash,0,65),mapping_version:substring(candidate.mapping_version,0,65),relationship:substring(candidate.relationship,0,33),has_condition:candidate.has_condition,condition_keys:[key IN candidate.condition_keys[..129] | substring(key,0,257)],condition_key_count:size(candidate.condition_keys),condition_key_lengths:[key IN candidate.condition_keys[..129] | size(key)],resource_scope:substring(candidate.resource_scope,0,33),assessment:substring(candidate.assessment,0,33),evidence_kind:substring(candidate.evidence_kind,0,65)} END AS candidate
229
+ LIMIT $policyRowLimit`;
230
+ export const POLICY_COLUMNS = ['key', 'bindingProof', 'identity', 'resource', 'identityLink', 'resourceLink', 'principal', 'bucket',
231
+ 'principalAccount', 'bucketAccount', 'principalOwnership', 'bucketOwnership', 'collection', 'principalMember', 'bucketMember', 'candidate'];
232
+ export function policyParameters(plan) {
233
+ return plan.probes.map(probe => ({ ...probe.proof, key: probe.key, identityNodeId: probe.identity.id, resourceNodeId: probe.resource.id,
234
+ accountId: probe.accountId, partition: probe.partition }));
235
+ }
236
+ export function safePolicyRow(raw) {
237
+ try {
238
+ return immutableJson(raw, 8, 10_000, 128 * 1024);
239
+ }
240
+ catch {
241
+ return fail('MAP_LIMIT_EXCEEDED');
242
+ }
243
+ }
244
+ function object(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null; }
245
+ function stamp(value, now) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= now; }
246
+ function hash(value) { return typeof value === 'string' && HASH.test(value); }
247
+ function unicodeOrder(a, b) {
248
+ const left = [...a], right = [...b];
249
+ for (let index = 0; index < Math.min(left.length, right.length); index++) {
250
+ const difference = left[index].codePointAt(0) - right[index].codePointAt(0);
251
+ if (difference)
252
+ return difference;
253
+ }
254
+ return left.length - right.length;
255
+ }
256
+ function referenceMatches(actual, expected) {
257
+ if (actual.id !== expected.id || actual.entity_id !== expected.entityId)
258
+ return false;
259
+ return referenceFields.filter(field => field !== 'id' && field !== 'entity_id')
260
+ .every(field => (actual[field] ?? null) === (expected.properties[field] ?? null));
261
+ }
262
+ /** Pure classification: no positive candidate survives a malformed endpoint or stale receipt. */
263
+ export function policyEvidence(plan, rows, now, maximumAge) {
264
+ const groups = new Map();
265
+ const requested = new Set(plan.probes.map(probe => probe.key));
266
+ for (const row of rows) {
267
+ const key = row.key;
268
+ if (typeof key !== 'number' || !Number.isSafeInteger(key) || !requested.has(key))
269
+ fail();
270
+ const items = groups.get(key) ?? [];
271
+ items.push(row);
272
+ groups.set(key, items);
273
+ }
274
+ const result = [...plan.records];
275
+ for (const probe of plan.probes) {
276
+ const base = result[probe.key], matches = groups.get(probe.key) ?? [];
277
+ const set = (status, code, time = null) => { result[probe.key] = reason(base, status, code, time); };
278
+ if (!matches.length) {
279
+ set('unavailable', 'changed');
280
+ continue;
281
+ }
282
+ const row = matches[0];
283
+ if (row.bindingProof !== true) {
284
+ set('unavailable', 'changed');
285
+ continue;
286
+ }
287
+ const structural = ({ candidate: _candidate, ...rest }) => JSON.stringify(rest);
288
+ if (matches.some(item => structural(item) !== structural(row))) {
289
+ set('unavailable', 'evidence_invalid');
290
+ continue;
291
+ }
292
+ const i = object(row.identity), x = object(row.resource), p = object(row.principal), b = object(row.bucket);
293
+ const pa = object(row.principalAccount), ba = object(row.bucketAccount), po = object(row.principalOwnership), bo = object(row.bucketOwnership);
294
+ const ic = object(row.identityLink), bc = object(row.resourceLink), c = object(row.collection), pm = object(row.principalMember), bm = object(row.bucketMember);
295
+ if (!i || !x || !referenceMatches(i, probe.identity) || !referenceMatches(x, probe.resource)) {
296
+ set('unavailable', 'changed');
297
+ continue;
298
+ }
299
+ if (!p || !b || !pa || !ba || !po || !bo || !ic || !bc) {
300
+ set('unavailable', 'provider_missing');
301
+ continue;
302
+ }
303
+ const containment = x.kind !== 'bucket';
304
+ if (p.principal !== true || p.role !== (probe.principalLabel === 'AWSRole') || p.user !== (probe.principalLabel === 'AWSUser') ||
305
+ p.id !== probe.principalArn || p.arn !== probe.principalArn || b.bucket !== true || b.id !== probe.bucketName || b.arn !== probe.bucketArn ||
306
+ pa.id !== probe.accountId || ba.id !== probe.accountId || pa.eid !== ba.eid ||
307
+ i.cloud_provider_id !== probe.principalArn || i.cloud_provider_label !== probe.principalLabel || i.cloud_match_kind !== 'exact' ||
308
+ x.cloud_provider_id !== probe.bucketName || x.cloud_provider_label !== 'AWSS3Bucket' || x.cloud_match_kind !== (containment ? 'containment' : 'exact') ||
309
+ ic.kind !== 'G_CLOUD_MATCH' || bc.kind !== (containment ? 'G_CLOUD_CONTAINER' : 'G_CLOUD_MATCH') ||
310
+ [i, x, ic, bc].some(item => (item.cloud_assessment ?? item.assessment) !== 'unknown') ||
311
+ ic.evidence_kind !== 'provider_inventory' || bc.evidence_kind !== 'provider_inventory' ||
312
+ ic.observed_at !== i.cloud_observed_at || bc.observed_at !== x.cloud_observed_at ||
313
+ (x.region && x.region !== b.region)) {
314
+ set('unavailable', 'evidence_invalid');
315
+ continue;
316
+ }
317
+ if (!c) {
318
+ set('not_collected', 'collection_missing');
319
+ continue;
320
+ }
321
+ if (c.id !== probe.collectionId || c.schema_version !== 1 || c.status !== 'complete' || !hash(c.content_hash) || !stamp(c.captured_at, now)) {
322
+ set('unavailable', 'collection_invalid');
323
+ continue;
324
+ }
325
+ const observedAt = new Date(c.captured_at * 1000).toISOString();
326
+ const times = [p.lastupdated, b.lastupdated, pa.lastupdated, ba.lastupdated, po.lastupdated, bo.lastupdated,
327
+ i.cloud_observed_at, x.cloud_observed_at, i.cloud_node_updated, x.cloud_node_updated,
328
+ i.cloud_ownership_updated, x.cloud_ownership_updated, i.cloud_account_updated, x.cloud_account_updated];
329
+ if (times.some(time => !stamp(time, now))) {
330
+ set('unavailable', 'changed', observedAt);
331
+ continue;
332
+ }
333
+ if ([c.captured_at, ...times].some(time => now - time > maximumAge)) {
334
+ set('unavailable', 'stale', observedAt);
335
+ continue;
336
+ }
337
+ if (!pm || !bm || [pm, bm].some(member => member.capture_hash !== c.content_hash || member.captured_at !== c.captured_at) ||
338
+ p.lastupdated !== c.captured_at || b.lastupdated !== c.captured_at || po.lastupdated !== c.captured_at || bo.lastupdated !== c.captured_at ||
339
+ i.cloud_node_updated !== p.lastupdated || x.cloud_node_updated !== b.lastupdated ||
340
+ i.cloud_ownership_updated !== po.lastupdated || x.cloud_ownership_updated !== bo.lastupdated ||
341
+ i.cloud_account_updated !== pa.lastupdated || x.cloud_account_updated !== ba.lastupdated || bm.region !== b.region) {
342
+ set('unavailable', 'changed', observedAt);
343
+ continue;
344
+ }
345
+ if (c.policy_status === 'disabled' || c.policy_status === null || c.policy_status === undefined) {
346
+ set('not_collected', 'disabled', observedAt);
347
+ continue;
348
+ }
349
+ if (c.policy_status !== 'complete' || !hash(c.policy_hash) || c.policy_mapping_version !== VERSION ||
350
+ pm.policy_hash !== c.policy_hash || pm.policy_mapping_version !== VERSION || pm.policy_assessment !== 'unknown' ||
351
+ !['mapped', 'unsupported', 'boundary_unresolved'].includes(String(pm.policy_status)) ||
352
+ typeof pm.policy_boundary_present !== 'boolean' || pm.policy_boundary_present !== (pm.policy_status === 'boundary_unresolved')) {
353
+ set('unavailable', 'collection_invalid', observedAt);
354
+ continue;
355
+ }
356
+ if (pm.policy_status !== 'mapped') {
357
+ set('unavailable', pm.policy_status === 'boundary_unresolved' ? 'boundary_unresolved' : 'unsupported_policy', observedAt);
358
+ continue;
359
+ }
360
+ const candidates = [], seen = new Set();
361
+ let invalid = false;
362
+ for (const match of matches) {
363
+ const candidate = object(match.candidate);
364
+ if (!candidate) {
365
+ if (matches.length !== 1)
366
+ invalid = true;
367
+ continue;
368
+ }
369
+ const keys = candidate.condition_keys, lengths = candidate.condition_key_lengths;
370
+ const relation = candidate.relationship;
371
+ if (candidate.collection_id !== c.id || candidate.capture_hash !== c.content_hash || candidate.captured_at !== c.captured_at ||
372
+ candidate.policy_hash !== c.policy_hash || candidate.mapping_version !== VERSION || candidate.assessment !== 'unknown' ||
373
+ candidate.resource_scope !== 'bucket_summary' || candidate.evidence_kind !== 'iam_identity_policy_candidate' ||
374
+ typeof relation !== 'string' || !Object.values(MAPPING).includes(relation) || seen.has(relation) ||
375
+ typeof candidate.has_condition !== 'boolean' || !Array.isArray(keys) || keys.length > 128 ||
376
+ candidate.condition_key_count !== keys.length || !Array.isArray(lengths) || lengths.length !== keys.length ||
377
+ keys.some((key, index) => typeof key !== 'string' || !key || /[\u0000-\u001f\ud800-\udfff]/u.test(key) || Buffer.byteLength(key) > 256 || lengths[index] !== [...key].length) ||
378
+ new Set(keys).size !== keys.length || JSON.stringify(keys) !== JSON.stringify([...keys].sort(unicodeOrder)) || candidate.has_condition !== (keys.length > 0)) {
379
+ invalid = true;
380
+ break;
381
+ }
382
+ seen.add(relation);
383
+ if (relation === probe.relationship)
384
+ candidates.push({ relationship: relation, hasCondition: candidate.has_condition,
385
+ conditionKeys: keys, resourceScope: 'bucket_summary' });
386
+ }
387
+ if (invalid) {
388
+ set('unavailable', 'evidence_invalid', observedAt);
389
+ continue;
390
+ }
391
+ result[probe.key] = { ...base, status: 'complete', reason: candidates.length ? null : 'no_candidate', observedAt, candidates };
392
+ }
393
+ return result;
394
+ }
@@ -0,0 +1,28 @@
1
+ import { type JsonObject, type JsonValue, type MapEdge, type MapNode, type MapStore, type ReadScope } from './types.js';
2
+ export declare const MAP_OPERATIONS: readonly ['list_agents', 'get_agent_access', 'explain_access', 'find_agents_with_access', 'get_changes', 'get_inventory_health'];
3
+ export type MapOperation = typeof MAP_OPERATIONS[number];
4
+ export declare const MAP_INPUT_SCHEMAS: Readonly<Record<MapOperation, JsonObject>>;
5
+ export interface MapResponse {
6
+ readonly operation: MapOperation;
7
+ readonly observedAt: string;
8
+ readonly data: JsonObject;
9
+ readonly graph?: {
10
+ readonly nodes: readonly MapNode[];
11
+ readonly edges: readonly MapEdge[];
12
+ };
13
+ }
14
+ export interface MapServiceOptions {
15
+ readonly clock?: () => number;
16
+ readonly maxResponseBytes?: number;
17
+ readonly maxGraphNodes?: number;
18
+ readonly maxGraphEdges?: number;
19
+ readonly queryTimeoutMs?: number;
20
+ readonly staleAfterSeconds?: number;
21
+ }
22
+ export declare function validateQueryArguments(operation: unknown, value: unknown): Readonly<Record<string, JsonValue>>;
23
+ /** One read-only evidence layer shared by HTTP, MCP and the browser. */
24
+ export declare class MapService {
25
+ #private;
26
+ constructor(store: MapStore, options?: MapServiceOptions);
27
+ query(inputScope: ReadScope, inputOperation: unknown, inputArgs: unknown): Promise<MapResponse>;
28
+ }