@shieldaai/agent-map 0.1.0-alpha.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +652 -272
- package/dist/assets.js +16 -26
- package/dist/boundary-flags.d.ts +73 -0
- package/dist/boundary-flags.js +352 -0
- package/dist/boundary.d.ts +18 -3
- package/dist/boundary.js +71 -21
- package/dist/cli-entry.d.ts +11 -0
- package/dist/cli-entry.js +48 -0
- package/dist/cli.js +40 -43
- package/dist/common.d.ts +34 -0
- package/dist/common.js +94 -0
- package/dist/http.js +78 -51
- package/dist/identity-cli.js +93 -46
- package/dist/identity-http.js +226 -94
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/mcp.js +40 -5
- package/dist/oidc.d.ts +5 -0
- package/dist/oidc.js +121 -38
- package/dist/policy-reader.d.ts +56 -3
- package/dist/policy-reader.js +823 -110
- package/dist/projection-fields.d.ts +4 -0
- package/dist/projection-fields.js +144 -0
- package/dist/service/changes.d.ts +5 -0
- package/dist/service/changes.js +255 -0
- package/dist/service/context.d.ts +33 -0
- package/dist/service/context.js +2 -0
- package/dist/service/discovery.d.ts +6 -0
- package/dist/service/discovery.js +31 -0
- package/dist/service/explain.d.ts +12 -0
- package/dist/service/explain.js +126 -0
- package/dist/service/flags.d.ts +20 -0
- package/dist/service/flags.js +70 -0
- package/dist/service/inventory.d.ts +4 -0
- package/dist/service/inventory.js +62 -0
- package/dist/service/json.d.ts +8 -0
- package/dist/service/json.js +7 -0
- package/dist/service/operations/discovery.d.ts +5 -0
- package/dist/service/operations/discovery.js +90 -0
- package/dist/service/operations/explain-access.d.ts +3 -0
- package/dist/service/operations/explain-access.js +25 -0
- package/dist/service/operations/find-agents-with-access.d.ts +3 -0
- package/dist/service/operations/find-agents-with-access.js +120 -0
- package/dist/service/operations/get-agent-access.d.ts +3 -0
- package/dist/service/operations/get-agent-access.js +75 -0
- package/dist/service/operations/get-agent-inventory.d.ts +3 -0
- package/dist/service/operations/get-agent-inventory.js +42 -0
- package/dist/service/operations/get-changes.d.ts +3 -0
- package/dist/service/operations/get-changes.js +11 -0
- package/dist/service/operations/get-inventory-health.d.ts +3 -0
- package/dist/service/operations/get-inventory-health.js +81 -0
- package/dist/service/operations/get-resource-access.d.ts +3 -0
- package/dist/service/operations/get-resource-access.js +48 -0
- package/dist/service/operations/list-agents.d.ts +3 -0
- package/dist/service/operations/list-agents.js +92 -0
- package/dist/service/operations/list-owners-resources.d.ts +5 -0
- package/dist/service/operations/list-owners-resources.js +69 -0
- package/dist/service/ordering.d.ts +9 -0
- package/dist/service/ordering.js +47 -0
- package/dist/service/outside-guard.d.ts +68 -0
- package/dist/service/outside-guard.js +313 -0
- package/dist/service/policy.d.ts +26 -0
- package/dist/service/policy.js +299 -0
- package/dist/service/schemas.d.ts +16 -0
- package/dist/service/schemas.js +192 -0
- package/dist/service/summaries.d.ts +32 -0
- package/dist/service/summaries.js +247 -0
- package/dist/service/work.d.ts +33 -0
- package/dist/service/work.js +116 -0
- package/dist/service.d.ts +5 -5
- package/dist/service.js +110 -645
- package/dist/store/ancestors.d.ts +9 -0
- package/dist/store/ancestors.js +86 -0
- package/dist/store/discovery.d.ts +8 -0
- package/dist/store/discovery.js +288 -0
- package/dist/store/evidence.d.ts +12 -0
- package/dist/store/evidence.js +90 -0
- package/dist/store/identity-scope.d.ts +9 -0
- package/dist/store/identity-scope.js +71 -0
- package/dist/store/nodes.d.ts +24 -0
- package/dist/store/nodes.js +237 -0
- package/dist/store/projection.d.ts +19 -0
- package/dist/store/projection.js +242 -0
- package/dist/store/queries.d.ts +48 -0
- package/dist/store/queries.js +302 -0
- package/dist/store/read.d.ts +81 -0
- package/dist/store/read.js +135 -0
- package/dist/store/selection.d.ts +3 -0
- package/dist/store/selection.js +102 -0
- package/dist/store.d.ts +15 -0
- package/dist/store.js +102 -395
- package/dist/transport.d.ts +42 -2
- package/dist/transport.js +144 -16
- package/dist/types.d.ts +184 -4
- package/dist/types.js +52 -11
- package/package.json +16 -3
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GuardLabel, JsonObject, MapNode } from './types.js';
|
|
2
|
+
export declare const MAP_PROPERTIES: Readonly<Record<GuardLabel, readonly string[]>>;
|
|
3
|
+
/** Defense in depth for summaries/diffs, including host-provided MapStore adapters. */
|
|
4
|
+
export declare function safeProperties(node: MapNode): JsonObject;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const CLOUD_FIELDS = [
|
|
2
|
+
'cloud_status',
|
|
3
|
+
'cloud_reason',
|
|
4
|
+
'cloud_observed_at',
|
|
5
|
+
'cloud_assessment',
|
|
6
|
+
'cloud_provider_label',
|
|
7
|
+
'cloud_provider_id',
|
|
8
|
+
'cloud_match_kind',
|
|
9
|
+
'cloud_node_updated',
|
|
10
|
+
'cloud_ownership_updated',
|
|
11
|
+
'cloud_account_updated',
|
|
12
|
+
];
|
|
13
|
+
const HOST_FIELDS = [
|
|
14
|
+
'evidence_kind',
|
|
15
|
+
'assessment',
|
|
16
|
+
'manifest_id',
|
|
17
|
+
'manifest_revision',
|
|
18
|
+
'manifest_observed_at',
|
|
19
|
+
'authority_id',
|
|
20
|
+
'manifest_schema_version',
|
|
21
|
+
];
|
|
22
|
+
export const MAP_PROPERTIES = Object.freeze({
|
|
23
|
+
GuardAgent: [
|
|
24
|
+
'agent_id',
|
|
25
|
+
'name',
|
|
26
|
+
'owner_id',
|
|
27
|
+
'team_id',
|
|
28
|
+
'purpose',
|
|
29
|
+
'lifecycle_state',
|
|
30
|
+
'ownership_status',
|
|
31
|
+
'ownership_source',
|
|
32
|
+
...HOST_FIELDS,
|
|
33
|
+
],
|
|
34
|
+
GuardDeployment: [
|
|
35
|
+
'deployment_id',
|
|
36
|
+
'environment',
|
|
37
|
+
'runtime_ref',
|
|
38
|
+
'workload_generation',
|
|
39
|
+
'build_ref',
|
|
40
|
+
'binding_authority',
|
|
41
|
+
// Manifest v3 only: 'found' | 'none_declared'; absent means unknown.
|
|
42
|
+
'direct_access_status',
|
|
43
|
+
...HOST_FIELDS,
|
|
44
|
+
],
|
|
45
|
+
GuardTask: [
|
|
46
|
+
'task_id',
|
|
47
|
+
'principal_id',
|
|
48
|
+
'revoked',
|
|
49
|
+
'delegated',
|
|
50
|
+
'derived',
|
|
51
|
+
'sensitive',
|
|
52
|
+
'untrusted',
|
|
53
|
+
'allowed_recipients',
|
|
54
|
+
'allow_control_plane',
|
|
55
|
+
'max_calls',
|
|
56
|
+
'max_concurrent',
|
|
57
|
+
'max_cost',
|
|
58
|
+
'deadline',
|
|
59
|
+
'calls',
|
|
60
|
+
'concurrent',
|
|
61
|
+
'spent',
|
|
62
|
+
'reserved',
|
|
63
|
+
],
|
|
64
|
+
GuardPrincipal: ['principal_id'],
|
|
65
|
+
GuardGrant: ['task_id', 'tool', 'resources', 'evidence_kind', 'assessment'],
|
|
66
|
+
GuardTool: [
|
|
67
|
+
'name',
|
|
68
|
+
'fingerprint',
|
|
69
|
+
'revision',
|
|
70
|
+
'registered',
|
|
71
|
+
'approved',
|
|
72
|
+
'known_metadata',
|
|
73
|
+
'requires_approval',
|
|
74
|
+
'control_plane',
|
|
75
|
+
'max_cost',
|
|
76
|
+
// Inventory v2 outside-content labels and the executor default; absent for v1.
|
|
77
|
+
'reads_untrusted',
|
|
78
|
+
'untrusted_context',
|
|
79
|
+
'sends_external',
|
|
80
|
+
'untrusted_context_default',
|
|
81
|
+
],
|
|
82
|
+
GuardResource: ['resource_id', 'tool', 'selector_kind'],
|
|
83
|
+
GuardResourceBinding: [
|
|
84
|
+
'binding_id',
|
|
85
|
+
'tool',
|
|
86
|
+
'resource_ref',
|
|
87
|
+
'actions',
|
|
88
|
+
'scope',
|
|
89
|
+
'display_name',
|
|
90
|
+
'owner_id',
|
|
91
|
+
'ownership_status',
|
|
92
|
+
'ownership_source',
|
|
93
|
+
'sensitivity',
|
|
94
|
+
'approved_destinations',
|
|
95
|
+
...HOST_FIELDS,
|
|
96
|
+
],
|
|
97
|
+
GuardExecutionBinding: ['binding_id', 'tool', 'connection_id', 'identity_selection', ...HOST_FIELDS],
|
|
98
|
+
GuardExternalResource: [
|
|
99
|
+
'provider',
|
|
100
|
+
'partition',
|
|
101
|
+
'account_id',
|
|
102
|
+
'region',
|
|
103
|
+
'arn',
|
|
104
|
+
'kind',
|
|
105
|
+
'host_resource_id',
|
|
106
|
+
'name',
|
|
107
|
+
...HOST_FIELDS,
|
|
108
|
+
...CLOUD_FIELDS,
|
|
109
|
+
],
|
|
110
|
+
GuardExternalIdentity: ['provider', 'partition', 'account_id', 'region', 'arn', ...HOST_FIELDS, ...CLOUD_FIELDS],
|
|
111
|
+
GuardMcpConnection: [
|
|
112
|
+
'connection_id',
|
|
113
|
+
'capture_status',
|
|
114
|
+
'unavailable_reason',
|
|
115
|
+
'capture_observed_at',
|
|
116
|
+
'server_id',
|
|
117
|
+
'transport_ref',
|
|
118
|
+
'state',
|
|
119
|
+
'metadata_state',
|
|
120
|
+
'observed_at',
|
|
121
|
+
'evidence_kind',
|
|
122
|
+
'assessment',
|
|
123
|
+
],
|
|
124
|
+
GuardOwner: ['owner_id', 'name', 'kind', ...HOST_FIELDS],
|
|
125
|
+
GuardCredentialReference: [
|
|
126
|
+
'credential_id',
|
|
127
|
+
'name',
|
|
128
|
+
'issuer_ref',
|
|
129
|
+
'audience_refs',
|
|
130
|
+
'recipient_refs',
|
|
131
|
+
'lifecycle_state',
|
|
132
|
+
'expires_at',
|
|
133
|
+
'rotation_status',
|
|
134
|
+
...HOST_FIELDS,
|
|
135
|
+
],
|
|
136
|
+
GuardDirectAccess: ['access_id', 'deployment_id', 'kind', 'target_ref', 'description', ...HOST_FIELDS],
|
|
137
|
+
GuardMcpOperation: ['connection_id', 'name', 'kind', 'fingerprint', 'evidence_kind', 'assessment'],
|
|
138
|
+
});
|
|
139
|
+
/** Defense in depth for summaries/diffs, including host-provided MapStore adapters. */
|
|
140
|
+
export function safeProperties(node) {
|
|
141
|
+
return Object.fromEntries((MAP_PROPERTIES[node.label] ?? [])
|
|
142
|
+
.filter(key => Object.hasOwn(node.properties, key))
|
|
143
|
+
.map(key => [key, node.properties[key]]));
|
|
144
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { JsonObject, JsonValue, MapNode, MapProjection, MapSource, ReadScope } from '../types.js';
|
|
2
|
+
import { QueryWork } from './work.js';
|
|
3
|
+
export declare const IGNORE_DIFF: Set<string>;
|
|
4
|
+
export declare function nodeState(node: MapNode, projection: MapProjection, work: QueryWork): string;
|
|
5
|
+
export declare function retainedChanges(source: MapSource, args: Readonly<Record<string, JsonValue>>, scope: ReadScope, work: QueryWork): JsonObject;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { canonicalJson } from '@shieldaai/agent-guard/internal';
|
|
2
|
+
import { fail } from '../common.js';
|
|
3
|
+
import { safeProperties } from '../projection-fields.js';
|
|
4
|
+
import { compare, ordered, page } from './ordering.js';
|
|
5
|
+
import { targetVisibility } from './outside-guard.js';
|
|
6
|
+
import { changeSummary, collection, focus, receipt, relationshipRows } from './summaries.js';
|
|
7
|
+
import { connected, matching, nodeIds, QueryWork, string } from './work.js';
|
|
8
|
+
export const IGNORE_DIFF = new Set([
|
|
9
|
+
'lastupdated',
|
|
10
|
+
'manifest_observed_at',
|
|
11
|
+
'manifest_revision',
|
|
12
|
+
'observed_at',
|
|
13
|
+
'capture_observed_at',
|
|
14
|
+
'cloud_observed_at',
|
|
15
|
+
// Recorded since 1.0.0; they cannot change for a task, so they are never a change.
|
|
16
|
+
'delegated',
|
|
17
|
+
'derived',
|
|
18
|
+
]);
|
|
19
|
+
export function nodeState(node, projection, work) {
|
|
20
|
+
const index = work.index(projection);
|
|
21
|
+
const properties = Object.fromEntries(Object.entries(safeProperties(node)).filter(([key]) => !IGNORE_DIFF.has(key)));
|
|
22
|
+
const edges = [...(index.outgoing.get(node.id)?.entries() ?? [])]
|
|
23
|
+
.flatMap(([kind, targets]) => {
|
|
24
|
+
work.step(targets.length);
|
|
25
|
+
return targets.map(target => ({ kind, target: index.nodes.get(target)?.entityId ?? null }));
|
|
26
|
+
})
|
|
27
|
+
.sort((a, b) => compare(`${a.kind}:${a.target}`, `${b.kind}:${b.target}`));
|
|
28
|
+
return canonicalJson({ properties, edges });
|
|
29
|
+
}
|
|
30
|
+
export function retainedChanges(source, args, scope, work) {
|
|
31
|
+
const current = source.current, since = args.since ?? 'previous';
|
|
32
|
+
const previous = [current.id, current.snapshotId].includes(since)
|
|
33
|
+
? current
|
|
34
|
+
: source.previous &&
|
|
35
|
+
(since === 'previous' || [source.previous.id, source.previous.snapshotId].includes(since))
|
|
36
|
+
? source.previous
|
|
37
|
+
: null;
|
|
38
|
+
const projections = previous && previous !== current ? [current, previous] : [current];
|
|
39
|
+
const related = new Map();
|
|
40
|
+
for (const projection of projections) {
|
|
41
|
+
const impacted = new Map();
|
|
42
|
+
for (const agent of projection.nodes.filter(node => node.label === 'GuardAgent')) {
|
|
43
|
+
const brief = {
|
|
44
|
+
id: agent.entityId,
|
|
45
|
+
agentId: string(agent, 'agent_id'),
|
|
46
|
+
name: string(agent, 'name'),
|
|
47
|
+
sourceId: source.id,
|
|
48
|
+
};
|
|
49
|
+
for (const node of focus(projection, agent, work).nodes) {
|
|
50
|
+
const owners = impacted.get(node.entityId) ?? [];
|
|
51
|
+
owners.push(brief);
|
|
52
|
+
impacted.set(node.entityId, owners);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
related.set(projection, impacted);
|
|
56
|
+
}
|
|
57
|
+
const selectedAgents = new Set(), selectedResources = new Set();
|
|
58
|
+
for (const projection of projections) {
|
|
59
|
+
for (const node of projection.nodes) {
|
|
60
|
+
work.step();
|
|
61
|
+
if (args.agentId && node.label === 'GuardAgent' && matching(node, args.agentId, 'agent_id'))
|
|
62
|
+
selectedAgents.add(node.entityId);
|
|
63
|
+
if (args.resourceId &&
|
|
64
|
+
node.label === 'GuardResourceBinding' &&
|
|
65
|
+
matching(node, args.resourceId, 'binding_id')) {
|
|
66
|
+
const selectors = connected(projection, new Set([node.id]), 'G_BINDS_SELECTOR', work);
|
|
67
|
+
const grants = connected(projection, nodeIds(selectors), 'G_TARGETS', work, true);
|
|
68
|
+
const tasks = connected(projection, nodeIds(grants), 'G_HAS_GRANT', work, true);
|
|
69
|
+
const deployment = connected(projection, nodeIds(tasks), 'G_ASSIGNED_TO', work);
|
|
70
|
+
const tools = connected(projection, nodeIds(grants), 'G_FOR_TOOL', work);
|
|
71
|
+
const toolIds = nodeIds(tools);
|
|
72
|
+
const executions = connected(projection, nodeIds(deployment), 'G_EXECUTES_FOR', work, true).filter(execution => connected(projection, new Set([execution.id]), 'G_EXECUTES_TOOL', work).some(tool => toolIds.has(tool.id)));
|
|
73
|
+
for (const item of [
|
|
74
|
+
node,
|
|
75
|
+
...selectors,
|
|
76
|
+
...grants,
|
|
77
|
+
...tasks,
|
|
78
|
+
...deployment,
|
|
79
|
+
...tools,
|
|
80
|
+
...executions,
|
|
81
|
+
...connected(projection, new Set([node.id]), 'G_MAPS_TO', work),
|
|
82
|
+
...connected(projection, new Set([node.id]), 'G_RESOURCE_OWNED_BY', work),
|
|
83
|
+
...connected(projection, nodeIds(executions), 'G_USES_IDENTITY', work),
|
|
84
|
+
])
|
|
85
|
+
selectedResources.add(item.entityId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if ((args.agentId && !selectedAgents.size) || (args.resourceId && !selectedResources.size))
|
|
90
|
+
fail('MAP_NOT_FOUND');
|
|
91
|
+
// Callers list the current snapshot's entries first, and the first entry for an id wins, so
|
|
92
|
+
// a renamed agent appears under its current name.
|
|
93
|
+
const uniqueAgents = (items) => {
|
|
94
|
+
const first = new Map();
|
|
95
|
+
for (const item of items)
|
|
96
|
+
if (!first.has(item.id))
|
|
97
|
+
first.set(item.id, item);
|
|
98
|
+
return ordered([...first.values()]);
|
|
99
|
+
};
|
|
100
|
+
const before = new Map(previous?.nodes.map(node => [node.entityId, node]) ?? []), after = new Map(current.nodes.map(node => [node.entityId, node]));
|
|
101
|
+
const beforeEdges = previous ? relationshipRows(previous, work) : [], afterEdges = relationshipRows(current, work);
|
|
102
|
+
const edgeKey = (edge) => JSON.stringify([edge.source, edge.target, edge.kind]);
|
|
103
|
+
const oldEdges = new Set(beforeEdges.map(edgeKey)), newEdges = new Set(afterEdges.map(edgeKey));
|
|
104
|
+
const relationships = {
|
|
105
|
+
added: previous ? afterEdges.filter(edge => !oldEdges.has(edgeKey(edge))) : [],
|
|
106
|
+
removed: previous ? beforeEdges.filter(edge => !newEdges.has(edgeKey(edge))) : [],
|
|
107
|
+
};
|
|
108
|
+
const outgoing = (edges) => {
|
|
109
|
+
const result = new Map();
|
|
110
|
+
for (const edge of edges) {
|
|
111
|
+
const list = result.get(edge.source) ?? [];
|
|
112
|
+
list.push(edge);
|
|
113
|
+
result.set(edge.source, list);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
};
|
|
117
|
+
const oldOutgoing = outgoing(beforeEdges), newOutgoing = outgoing(afterEdges), changedOutgoing = outgoing([...relationships.added, ...relationships.removed]);
|
|
118
|
+
// A declared path's target is shown only when that resource is in the reader's scope.
|
|
119
|
+
const visibleNow = targetVisibility(current, scope, work), visibleBefore = previous ? targetVisibility(previous, scope, work) : visibleNow;
|
|
120
|
+
const state = (node, edges, visible) => node
|
|
121
|
+
? {
|
|
122
|
+
properties: Object.fromEntries(Object.entries(safeProperties(node)).filter(([key, value]) => !IGNORE_DIFF.has(key) &&
|
|
123
|
+
!(node.label === 'GuardDirectAccess' &&
|
|
124
|
+
key === 'target_ref' &&
|
|
125
|
+
typeof value === 'string' &&
|
|
126
|
+
!visible(value)))),
|
|
127
|
+
relationships: edges.get(node.entityId) ?? [],
|
|
128
|
+
}
|
|
129
|
+
: null;
|
|
130
|
+
const details = [];
|
|
131
|
+
if (previous)
|
|
132
|
+
for (const id of new Set([...after.keys(), ...before.keys()])) {
|
|
133
|
+
work.step();
|
|
134
|
+
const old = before.get(id), node = after.get(id), display = node ?? old;
|
|
135
|
+
if (node && old && nodeState(node, current, work) === nodeState(old, previous, work))
|
|
136
|
+
continue;
|
|
137
|
+
const oldState = state(old, oldOutgoing, visibleBefore), newState = state(node, newOutgoing, visibleNow);
|
|
138
|
+
const oldProps = oldState?.properties, newProps = newState?.properties;
|
|
139
|
+
const fields = [...new Set([...Object.keys(oldProps ?? {}), ...Object.keys(newProps ?? {})])]
|
|
140
|
+
.filter(key => canonicalJson((oldProps?.[key] ?? null)) !== canonicalJson((newProps?.[key] ?? null)))
|
|
141
|
+
.sort();
|
|
142
|
+
const edgeChanges = changedOutgoing.get(id) ?? [];
|
|
143
|
+
const categories = [];
|
|
144
|
+
if (display.label === 'GuardOwner' ||
|
|
145
|
+
fields.some(field => ['owner_id', 'team_id', 'ownership_status', 'ownership_source'].includes(field)) ||
|
|
146
|
+
edgeChanges.some(edge => ['G_OWNED_BY', 'G_RESOURCE_OWNED_BY'].includes(edge.kind)))
|
|
147
|
+
categories.push('ownership');
|
|
148
|
+
if (['GuardGrant', 'GuardTool', 'GuardTask'].includes(display.label) ||
|
|
149
|
+
edgeChanges.some(edge => ['G_HAS_GRANT', 'G_FOR_TOOL', 'G_TARGETS'].includes(edge.kind)))
|
|
150
|
+
categories.push('permissions');
|
|
151
|
+
if ([
|
|
152
|
+
'GuardDeployment',
|
|
153
|
+
'GuardResourceBinding',
|
|
154
|
+
'GuardExecutionBinding',
|
|
155
|
+
'GuardExternalResource',
|
|
156
|
+
'GuardExternalIdentity',
|
|
157
|
+
'GuardCredentialReference',
|
|
158
|
+
'GuardMcpConnection',
|
|
159
|
+
'GuardMcpOperation',
|
|
160
|
+
].includes(display.label) ||
|
|
161
|
+
edgeChanges.some(edge => ![
|
|
162
|
+
'G_OWNED_BY',
|
|
163
|
+
'G_RESOURCE_OWNED_BY',
|
|
164
|
+
'G_HAS_GRANT',
|
|
165
|
+
'G_FOR_TOOL',
|
|
166
|
+
'G_TARGETS',
|
|
167
|
+
'G_DIRECT_ACCESS_FOR',
|
|
168
|
+
].includes(edge.kind)))
|
|
169
|
+
categories.push('bindings');
|
|
170
|
+
// Host-declared paths around the guard, and a deployment's declaration status.
|
|
171
|
+
const outsideGuard = display.label === 'GuardDirectAccess' ||
|
|
172
|
+
fields.includes('direct_access_status') ||
|
|
173
|
+
edgeChanges.some(edge => edge.kind === 'G_DIRECT_ACCESS_FOR');
|
|
174
|
+
if (outsideGuard)
|
|
175
|
+
categories.push('outside_guard');
|
|
176
|
+
if (!categories.length)
|
|
177
|
+
categories.push('inventory');
|
|
178
|
+
const affected = uniqueAgents([
|
|
179
|
+
...(related.get(current)?.get(id) ?? []),
|
|
180
|
+
...(related.get(previous)?.get(id) ?? []),
|
|
181
|
+
]);
|
|
182
|
+
if (args.agentId && !affected.some(agent => selectedAgents.has(agent.id)))
|
|
183
|
+
continue;
|
|
184
|
+
if (args.resourceId && !selectedResources.has(id))
|
|
185
|
+
continue;
|
|
186
|
+
if (args.changeKind && args.changeKind !== 'all' && !categories.includes(args.changeKind))
|
|
187
|
+
continue;
|
|
188
|
+
const navigation = display.label === 'GuardOwner' && string(display, 'owner_id')
|
|
189
|
+
? {
|
|
190
|
+
operation: 'list_agents',
|
|
191
|
+
args: { sourceId: source.id, ownerId: string(display, 'owner_id') },
|
|
192
|
+
scope: 'direct_current_ownership',
|
|
193
|
+
limitation: 'Lists agents directly owned now; excludes indirect impact through resource ownership.',
|
|
194
|
+
}
|
|
195
|
+
: display.label === 'GuardResourceBinding' && node
|
|
196
|
+
? {
|
|
197
|
+
operation: 'get_resource_access',
|
|
198
|
+
args: { sourceId: source.id, resourceId: node.entityId },
|
|
199
|
+
scope: 'current_observations',
|
|
200
|
+
}
|
|
201
|
+
: null;
|
|
202
|
+
details.push({
|
|
203
|
+
...changeSummary(display),
|
|
204
|
+
kind: !old ? 'added' : !node ? 'removed' : 'changed',
|
|
205
|
+
categories,
|
|
206
|
+
...(outsideGuard ? { evidenceKind: 'declared' } : {}),
|
|
207
|
+
changedFields: fields,
|
|
208
|
+
before: oldState,
|
|
209
|
+
after: newState,
|
|
210
|
+
affectedAgents: affected.slice(0, 100),
|
|
211
|
+
affectedAgentsTotal: affected.length,
|
|
212
|
+
affectedAgentsTruncated: affected.length > 100,
|
|
213
|
+
affectedAgentNavigation: navigation,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
ordered(details);
|
|
217
|
+
const { items, ...pagination } = page(details, args);
|
|
218
|
+
const explicitlyPaged = args.offset !== undefined || args.limit !== undefined;
|
|
219
|
+
const companion = explicitlyPaged ? items : details;
|
|
220
|
+
const changedIds = new Set(companion.map(item => item.entityId));
|
|
221
|
+
const relevantEdge = (edge) => changedIds.has(edge.source);
|
|
222
|
+
const affected = uniqueAgents(companion.flatMap(item => [
|
|
223
|
+
...(related.get(current)?.get(item.entityId) ?? []),
|
|
224
|
+
...(previous ? (related.get(previous)?.get(item.entityId) ?? []) : []),
|
|
225
|
+
]));
|
|
226
|
+
return {
|
|
227
|
+
sourceId: source.id,
|
|
228
|
+
from: previous ? receipt(source, previous) : null,
|
|
229
|
+
to: receipt(source),
|
|
230
|
+
changes: Object.fromEntries(['added', 'removed', 'changed'].map(kind => [
|
|
231
|
+
kind,
|
|
232
|
+
companion
|
|
233
|
+
.filter(item => item.kind === kind)
|
|
234
|
+
.map(item => ({ entityId: item.entityId, label: item.label, name: item.name })),
|
|
235
|
+
])),
|
|
236
|
+
details: items,
|
|
237
|
+
...pagination,
|
|
238
|
+
relationships: {
|
|
239
|
+
added: relationships.added.filter(relevantEdge),
|
|
240
|
+
removed: relationships.removed.filter(relevantEdge),
|
|
241
|
+
},
|
|
242
|
+
changeSummaryScope: explicitlyPaged ? 'page' : 'retained',
|
|
243
|
+
affectedAgents: affected.slice(0, 100),
|
|
244
|
+
affectedAgentsTotal: affected.length,
|
|
245
|
+
affectedAgentsTruncated: affected.length > 100,
|
|
246
|
+
coverageGap: previous === null,
|
|
247
|
+
policyHistory: 'not_collected',
|
|
248
|
+
collection,
|
|
249
|
+
limitations: [
|
|
250
|
+
'Net changes in authorized retained snapshots only; not an invocation or intermediate-event log.',
|
|
251
|
+
'Declared permission and binding changes do not establish effective access gain or loss.',
|
|
252
|
+
'Current IAM policy observations are separate from retained Guard snapshots; policy history is not collected.',
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { JsonObject, MapReadRequest, MapView, ReadScope } from '../types.js';
|
|
2
|
+
import type { QueryArguments } from './schemas.js';
|
|
3
|
+
import type { FocusedGraph } from './summaries.js';
|
|
4
|
+
import { QueryWork } from './work.js';
|
|
5
|
+
/** What every operation handler is given: the validated request, the clock and the bounded reads. */
|
|
6
|
+
export interface OperationContext {
|
|
7
|
+
readonly scope: ReadScope;
|
|
8
|
+
readonly args: QueryArguments;
|
|
9
|
+
/** The service clock at the start of the query, in milliseconds. */
|
|
10
|
+
readonly now: number;
|
|
11
|
+
readonly work: QueryWork;
|
|
12
|
+
/** The response byte limit; separate work budgets are sized from it. */
|
|
13
|
+
readonly maxBytes: number;
|
|
14
|
+
readonly staleSeconds: number;
|
|
15
|
+
/** One store read under this query's scope, with the query timeout unless a shorter one is given. */
|
|
16
|
+
read(request: MapReadRequest, timeout?: number): Promise<MapView>;
|
|
17
|
+
}
|
|
18
|
+
export interface OperationResult {
|
|
19
|
+
data: JsonObject;
|
|
20
|
+
/** Only get_agent_access returns a graph; the service drops it when it is too large. */
|
|
21
|
+
graph?: FocusedGraph;
|
|
22
|
+
}
|
|
23
|
+
export interface Operation {
|
|
24
|
+
/**
|
|
25
|
+
* The read the service runs before the handler, or null when the handler plans its own
|
|
26
|
+
* reads. It also refuses argument combinations that no read could answer.
|
|
27
|
+
*/
|
|
28
|
+
request(args: QueryArguments): MapReadRequest | null;
|
|
29
|
+
run(context: OperationContext, view: MapView): OperationResult | Promise<OperationResult>;
|
|
30
|
+
}
|
|
31
|
+
export declare const sourceSelection: (args: QueryArguments) => {
|
|
32
|
+
sourceId?: string;
|
|
33
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { JsonObject, MapSource, MapUndeclaredActivity } from '../types.js';
|
|
2
|
+
export declare const ALL_TEAMS_NOTE = "Shown only to readers with all-teams visibility, because what it lists belongs to no team. This note is the same whether or not anything would be listed.";
|
|
3
|
+
export declare const notVisible: () => JsonObject;
|
|
4
|
+
export declare function undeclaredCounts(source: MapSource, activity: MapUndeclaredActivity): JsonObject;
|
|
5
|
+
export declare const UNDECLARED_LIMITATIONS: string[];
|
|
6
|
+
export declare const UNCLAIMED_LIMITATIONS: string[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Discovery views list what belongs to no team, so only an all-teams reader sees them. A
|
|
2
|
+
// team-scoped reader gets this fixed answer without any read: no names, no counts, and the
|
|
3
|
+
// same words whether or not anything would be listed.
|
|
4
|
+
export const ALL_TEAMS_NOTE = 'Shown only to readers with all-teams visibility, because what it lists belongs to no team. This note is the same whether or not anything would be listed.';
|
|
5
|
+
export const notVisible = () => ({
|
|
6
|
+
visibility: 'all_teams_required',
|
|
7
|
+
status: 'not_visible',
|
|
8
|
+
note: ALL_TEAMS_NOTE,
|
|
9
|
+
});
|
|
10
|
+
export function undeclaredCounts(source, activity) {
|
|
11
|
+
return {
|
|
12
|
+
// Counting stops at 10,000: a minimum is partial, not complete.
|
|
13
|
+
status: activity.tasksCapped || activity.principalsCapped ? 'partial' : 'complete',
|
|
14
|
+
tasks: activity.tasks,
|
|
15
|
+
tasksIsMinimum: activity.tasksCapped,
|
|
16
|
+
principals: activity.principals,
|
|
17
|
+
principalsIsMinimum: activity.principalsCapped,
|
|
18
|
+
// Without a manifest no agent can be declared, so everything the executor saw counts.
|
|
19
|
+
manifest: source.profile === 'core' ? 'not_collected' : 'collected',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export const UNDECLARED_LIMITATIONS = [
|
|
23
|
+
'Only activity that ran through an enrolled executor is seen. An agent that does not use the guard leaves nothing here.',
|
|
24
|
+
'A task is undeclared when no manifest task binding assigns it to a deployment of a declared agent. parentDeclared marks a task whose direct parent is assigned: usually a sub-task that only lacks its own binding.',
|
|
25
|
+
'A lead to follow up, not a finding. Listing grants or blocks nothing.',
|
|
26
|
+
];
|
|
27
|
+
export const UNCLAIMED_LIMITATIONS = [
|
|
28
|
+
'Candidates to review, not findings: most IAM roles and users are not agents, and the map cannot tell which are.',
|
|
29
|
+
'An identity is claimed when a declared execution binding names its exact ARN in a current snapshot you can read. Role assumption, instance profiles and federation are not followed.',
|
|
30
|
+
'Only IAM roles and users of an enrolled AWS account are collected. An account that is missing, stale or inconsistent reads unknown, and its identities are not listed. Listing grants or blocks nothing.',
|
|
31
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { JsonObject, MapNode, MapProjection, ReadScope } from '../types.js';
|
|
2
|
+
import { QueryWork } from './work.js';
|
|
3
|
+
export declare function knownBlockers(task: MapNode, tool: MapNode | undefined, now: number): string[];
|
|
4
|
+
export declare function externalEvidence(projection: MapProjection, task: MapNode, toolName: string, resourceId: string, work: QueryWork, policySelection?: {
|
|
5
|
+
action?: string;
|
|
6
|
+
resourceBindingId?: string;
|
|
7
|
+
}): JsonObject;
|
|
8
|
+
export declare const COMMON_UNKNOWN: string[];
|
|
9
|
+
export declare function explain(projection: MapProjection, scope: ReadScope, task: MapNode, toolName: string, resourceId: string, now: number, work: QueryWork, policySelection?: {
|
|
10
|
+
action?: string;
|
|
11
|
+
resourceBindingId?: string;
|
|
12
|
+
}): JsonObject;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { safeProperties } from '../projection-fields.js';
|
|
2
|
+
import { jsonList } from './json.js';
|
|
3
|
+
import { policyCoverage, policyCoverageOf, selectedPolicy } from './policy.js';
|
|
4
|
+
import { grantSummary } from './summaries.js';
|
|
5
|
+
import { bindingKey, connected, nodeIds, number, QueryWork, string, strings, unique } from './work.js';
|
|
6
|
+
export function knownBlockers(task, tool, now) {
|
|
7
|
+
const blockers = [];
|
|
8
|
+
if (task.properties.revoked === true)
|
|
9
|
+
blockers.push('task_revoked');
|
|
10
|
+
const deadline = number(task, 'deadline');
|
|
11
|
+
if (deadline !== null && now >= deadline)
|
|
12
|
+
blockers.push('deadline_expired');
|
|
13
|
+
const calls = number(task, 'calls'), maxCalls = number(task, 'max_calls');
|
|
14
|
+
if (calls !== null && maxCalls !== null && calls >= maxCalls)
|
|
15
|
+
blockers.push('call_budget_exhausted');
|
|
16
|
+
const concurrent = number(task, 'concurrent'), maxConcurrent = number(task, 'max_concurrent');
|
|
17
|
+
if (concurrent !== null && maxConcurrent !== null && concurrent >= maxConcurrent)
|
|
18
|
+
blockers.push('concurrency_full_at_capture');
|
|
19
|
+
const spent = number(task, 'spent'), reserved = number(task, 'reserved'), maxCost = number(task, 'max_cost');
|
|
20
|
+
const cost = tool ? number(tool, 'max_cost') : null;
|
|
21
|
+
if (spent !== null && reserved !== null && maxCost !== null && cost !== null && spent + reserved + cost > maxCost)
|
|
22
|
+
blockers.push('cost_budget_exceeded');
|
|
23
|
+
if (tool?.properties.control_plane === true && task.properties.allow_control_plane === false)
|
|
24
|
+
blockers.push('control_plane_disabled');
|
|
25
|
+
return blockers;
|
|
26
|
+
}
|
|
27
|
+
export function externalEvidence(projection, task, toolName, resourceId, work, policySelection = {}) {
|
|
28
|
+
const deployment = connected(projection, new Set([task.id]), 'G_ASSIGNED_TO', work);
|
|
29
|
+
const executions = connected(projection, nodeIds(deployment), 'G_EXECUTES_FOR', work, true).filter(node => string(node, 'tool') === toolName);
|
|
30
|
+
const bound = work.lookup(projection).bindings.get(bindingKey(toolName, resourceId)) ?? [];
|
|
31
|
+
work.step(bound.length);
|
|
32
|
+
const resources = bound.filter(node => !policySelection.resourceBindingId || node.entityId === policySelection.resourceBindingId);
|
|
33
|
+
const policy = selectedPolicy(projection, work, {
|
|
34
|
+
task,
|
|
35
|
+
executions,
|
|
36
|
+
resources,
|
|
37
|
+
...(policySelection.action ? { action: policySelection.action } : {}),
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
// Allowlisted properties only, also for a host-provided store.
|
|
41
|
+
executionBindings: executions.map(node => ({
|
|
42
|
+
id: node.entityId,
|
|
43
|
+
...safeProperties(node),
|
|
44
|
+
identities: connected(projection, new Set([node.id]), 'G_USES_IDENTITY', work).map(safeProperties),
|
|
45
|
+
connections: connected(projection, new Set([node.id]), 'G_VIA_CONNECTION', work).map(safeProperties),
|
|
46
|
+
})),
|
|
47
|
+
resourceBindings: resources.map(node => ({
|
|
48
|
+
id: node.entityId,
|
|
49
|
+
...safeProperties(node),
|
|
50
|
+
targets: connected(projection, new Set([node.id]), 'G_MAPS_TO', work).map(safeProperties),
|
|
51
|
+
})),
|
|
52
|
+
policyEvidence: jsonList(policy),
|
|
53
|
+
policyCoverage: policyCoverageOf(projection, policy),
|
|
54
|
+
assessment: 'unknown',
|
|
55
|
+
limitations: [
|
|
56
|
+
'Host bindings and provider inventory are evidence, not effective cloud permissions.',
|
|
57
|
+
'Container matches do not establish exact object or prefix access; missing evidence does not establish denial.',
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export const COMMON_UNKNOWN = [
|
|
62
|
+
'live_authorization',
|
|
63
|
+
'request_arguments_and_resolution',
|
|
64
|
+
'custom_policy',
|
|
65
|
+
'backend_authorization',
|
|
66
|
+
'action_approval',
|
|
67
|
+
];
|
|
68
|
+
export function explain(projection, scope, task, toolName, resourceId, now, work, policySelection = {}) {
|
|
69
|
+
const index = work.index(projection), nodes = index.nodes;
|
|
70
|
+
const tool = work.lookup(projection).tools.get(toolName);
|
|
71
|
+
const blockers = [], matched = [], chain = [];
|
|
72
|
+
const unknown = [...COMMON_UNKNOWN];
|
|
73
|
+
if (!scope.allTeams)
|
|
74
|
+
unknown.push('ancestor_evidence_outside_visible_scope');
|
|
75
|
+
if (!tool || tool.properties.known_metadata === false || tool.properties.registered === false)
|
|
76
|
+
blockers.push('tool_unregistered_at_capture');
|
|
77
|
+
else if (tool.properties.registered !== true)
|
|
78
|
+
unknown.push('tool_registration');
|
|
79
|
+
if (tool?.properties.approved === false)
|
|
80
|
+
blockers.push('tool_metadata_unapproved');
|
|
81
|
+
else if (tool?.properties.approved !== true)
|
|
82
|
+
unknown.push('tool_metadata_approval');
|
|
83
|
+
const visited = new Set();
|
|
84
|
+
let current = task;
|
|
85
|
+
for (let depth = 0; current; depth++) {
|
|
86
|
+
work.step();
|
|
87
|
+
if (depth >= 16 || visited.has(current.id)) {
|
|
88
|
+
unknown.push('delegation_depth_or_cycle');
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
visited.add(current.id);
|
|
92
|
+
chain.push({ id: current.entityId, taskId: string(current, 'task_id') });
|
|
93
|
+
blockers.push(...knownBlockers(current, tool, now));
|
|
94
|
+
const grants = connected(projection, new Set([current.id]), 'G_HAS_GRANT', work).filter(node => node.label === 'GuardGrant' &&
|
|
95
|
+
string(node, 'tool') === toolName &&
|
|
96
|
+
strings(node, 'resources').some(resource => resource === resourceId || resource === '*'));
|
|
97
|
+
if (!grants.length)
|
|
98
|
+
blockers.push(current.id === task.id ? 'declared_grant_missing' : 'ancestor_declared_grant_missing');
|
|
99
|
+
matched.push(...grants.map(grantSummary));
|
|
100
|
+
const parents = index.outgoing.get(current.id)?.get('G_DELEGATED_FROM') ?? [];
|
|
101
|
+
work.step(parents.length);
|
|
102
|
+
if (parents.length > 1) {
|
|
103
|
+
unknown.push('delegation_evidence_ambiguous');
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
current = parents[0] ? nodes.get(parents[0]) : undefined;
|
|
107
|
+
}
|
|
108
|
+
if (task.properties.sensitive === true)
|
|
109
|
+
unknown.push('sensitive_egress_and_recipient_checks');
|
|
110
|
+
return {
|
|
111
|
+
taskId: string(task, 'task_id'),
|
|
112
|
+
tool: toolName,
|
|
113
|
+
resourceId,
|
|
114
|
+
assessment: blockers.length ? 'blocked' : 'unknown',
|
|
115
|
+
evidenceKind: 'declared_and_observed',
|
|
116
|
+
blockers: unique(blockers),
|
|
117
|
+
unknownChecks: unique(unknown),
|
|
118
|
+
delegation: chain,
|
|
119
|
+
matchedGrants: matched,
|
|
120
|
+
externalEvidence: externalEvidence(projection, task, toolName, resourceId, work, policySelection),
|
|
121
|
+
limitations: [
|
|
122
|
+
'Explanation only; no action was prepared or executed.',
|
|
123
|
+
'A clear exported gate does not establish effective permission.',
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type FlagGraph } from '../boundary-flags.js';
|
|
2
|
+
import { type JsonObject, type MapNode, type MapProjection, type ReadScope } from '../types.js';
|
|
3
|
+
import { QueryWork } from './work.js';
|
|
4
|
+
/**
|
|
5
|
+
* The bounded, read-only graph view the boundary flags use. Sensitive bindings for `*` grants
|
|
6
|
+
* come from the store's wildcard read when it ran (one-agent boundary reads); a team-wide read
|
|
7
|
+
* already holds every in-scope binding, indexed once per projection.
|
|
8
|
+
*/
|
|
9
|
+
export declare const sensitiveIndex: WeakMap<MapProjection, Map<string, {
|
|
10
|
+
id: string;
|
|
11
|
+
sensitivity: string;
|
|
12
|
+
}[]>>;
|
|
13
|
+
export declare function flagGraph(projection: MapProjection, work: QueryWork): FlagGraph;
|
|
14
|
+
/** Task expiry is judged at the snapshot's own capture time, so list and detail agree. */
|
|
15
|
+
export declare function agentFlags(projection: MapProjection, agent: MapNode, scope: ReadScope, work: QueryWork): import("../boundary-flags.js").DangerousThree;
|
|
16
|
+
/**
|
|
17
|
+
* The detail flag with its own work budget: a very large lineage degrades the flag to
|
|
18
|
+
* `unknown`, it never fails the rest of get_agent_access.
|
|
19
|
+
*/
|
|
20
|
+
export declare function detailFlags(projection: MapProjection, agent: MapNode, scope: ReadScope, remaining: number, maxBytes: number): JsonObject;
|