@shieldaai/agent-map 0.1.0-alpha.2 → 1.0.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 +653 -268
- 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 +19 -2
- package/dist/boundary.js +75 -19
- package/dist/cli-entry.d.ts +13 -0
- package/dist/cli-entry.js +66 -0
- package/dist/cli.js +41 -44
- package/dist/common.d.ts +34 -0
- package/dist/common.js +94 -0
- package/dist/http.js +78 -51
- package/dist/identity-cli.js +94 -47
- 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 +32 -5
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {} from '@shieldaai/agent-guard';
|
|
2
|
+
import { immutableJson } from '@shieldaai/agent-guard/internal';
|
|
3
|
+
import { fail } from '../common.js';
|
|
4
|
+
import { mapIdentifier } from '../types.js';
|
|
5
|
+
export const MAP_OPERATIONS = [
|
|
6
|
+
'list_agents',
|
|
7
|
+
'get_agent_access',
|
|
8
|
+
'explain_access',
|
|
9
|
+
'find_agents_with_access',
|
|
10
|
+
'get_changes',
|
|
11
|
+
'get_inventory_health',
|
|
12
|
+
'list_owners',
|
|
13
|
+
'list_resources',
|
|
14
|
+
'get_resource_access',
|
|
15
|
+
'get_agent_inventory',
|
|
16
|
+
'list_undeclared_activity',
|
|
17
|
+
'list_unclaimed_identities',
|
|
18
|
+
];
|
|
19
|
+
/** What each operation answers, in one sentence. Shown to MCP clients as the tool description. */
|
|
20
|
+
export const MAP_OPERATION_DESCRIPTIONS = Object.freeze({
|
|
21
|
+
list_agents: 'Lists the declared agents you may see, with owner, team, lifecycle and environments; filter by text, owner, team, environment, ownership, lifecycle or the dangerous-three boundary flag.',
|
|
22
|
+
get_agent_access: "Shows one agent's tasks, declared grants, bindings, MCP connections, IAM policy evidence, boundary flags and access outside the guard, with a graph of that neighborhood when it is small enough to draw. An empty policy list whose policyCoverage status is unknown was not evaluated; it does not mean no policy applies.",
|
|
23
|
+
explain_access: 'Explains, for one task, tool and resource selector, which declared grants match, which known blockers apply and which checks remain unknown; it never states that access is allowed.',
|
|
24
|
+
find_agents_with_access: 'Finds the agents whose declared grants and host bindings map to one exact resource reference or ARN for one cloud action; the results are candidates, not effective permissions.',
|
|
25
|
+
get_changes: "Compares a source's current snapshot with its previous retained one and lists what was added, removed or changed, optionally for one agent, one resource or one kind of change.",
|
|
26
|
+
get_inventory_health: 'Reports, for each source you may see, when each part of the inventory was captured, whether it is stale, how complete it is and how much activity no declared agent accounts for.',
|
|
27
|
+
list_owners: 'Lists the owners and teams that hosts name, with how many visible agents and resources each one owns.',
|
|
28
|
+
list_resources: 'Lists host-bound resources with their owner, sensitivity and scope, and whether something can reach each one outside the guard.',
|
|
29
|
+
get_resource_access: 'Shows one host-bound resource, the agents with a declared grant mapped to it, and the declared direct paths and tool identities that reach it outside the guard.',
|
|
30
|
+
get_agent_inventory: "Returns one page of one section of an agent's inventory (deployments, tasks, permissions, resources, connections, credentials, relationships, tools, bindings, identities or policies) with the size of every section. A size or total of null means that section was not evaluated, not that it is empty.",
|
|
31
|
+
list_undeclared_activity: 'Lists tasks or caller principals that ran through an enrolled executor but belong to no declared agent; it answers only for readers with all-teams visibility.',
|
|
32
|
+
list_unclaimed_identities: 'Lists IAM roles and users in enrolled AWS accounts that no declared agent names, as candidates to review; it answers only for readers with all-teams visibility.',
|
|
33
|
+
});
|
|
34
|
+
/** A parameter schema with the sentence MCP clients show for it. */
|
|
35
|
+
const described = (definition, description) => ({
|
|
36
|
+
...definition,
|
|
37
|
+
description,
|
|
38
|
+
});
|
|
39
|
+
const idSchema = { type: 'string', minLength: 1, maxLength: 4096 };
|
|
40
|
+
const searchSchema = { type: 'string', maxLength: 256 };
|
|
41
|
+
const limitSchema = { type: 'integer', minimum: 1, maximum: 100 };
|
|
42
|
+
const offsetSchema = { type: 'integer', minimum: 0, maximum: 100_000 };
|
|
43
|
+
const choice = (...values) => ({ type: 'string', enum: values });
|
|
44
|
+
const limit = (rows) => described(limitSchema, `The most ${rows} to return, 1 to 100 (default 50).`);
|
|
45
|
+
const offset = (rows) => described(offsetSchema, `How many ${rows} to skip, for paging (default 0).`);
|
|
46
|
+
const source = (purpose) => described(idSchema, `${purpose} Use the \`id\` of a source from get_inventory_health, or an agent's \`sourceId\`.`);
|
|
47
|
+
const agent = (purpose) => described(idSchema, `${purpose} Use the agent's \`id\` or its declared \`agentId\` from list_agents.`);
|
|
48
|
+
export const sections = [
|
|
49
|
+
'overview',
|
|
50
|
+
'owners',
|
|
51
|
+
'deployments',
|
|
52
|
+
'tasks',
|
|
53
|
+
'permissions',
|
|
54
|
+
'resources',
|
|
55
|
+
'connections',
|
|
56
|
+
'credentials',
|
|
57
|
+
'relationships',
|
|
58
|
+
'tools',
|
|
59
|
+
'bindings',
|
|
60
|
+
'identities',
|
|
61
|
+
'policies',
|
|
62
|
+
];
|
|
63
|
+
const schema = (properties, required = []) => immutableJson({ type: 'object', additionalProperties: false, properties, required });
|
|
64
|
+
export const MAP_INPUT_SCHEMAS = Object.freeze({
|
|
65
|
+
list_agents: schema({
|
|
66
|
+
search: described(searchSchema, 'Keep agents whose name or agent ID contains this text, ignoring case.'),
|
|
67
|
+
sourceId: source('Keep agents of this source only.'),
|
|
68
|
+
ownerId: described(idSchema, 'Keep agents whose declared owner has exactly this owner ID.'),
|
|
69
|
+
teamId: described(idSchema, 'Keep agents of exactly this team ID.'),
|
|
70
|
+
environment: described(idSchema, 'Keep agents with a deployment in exactly this environment.'),
|
|
71
|
+
ownershipStatus: described(choice('assigned', 'unassigned', 'unknown'), 'Keep agents whose ownership is assigned, unassigned or unknown.'),
|
|
72
|
+
lifecycleState: described(choice('active', 'inactive', 'retired', 'unknown'), 'Keep agents in this declared lifecycle state.'),
|
|
73
|
+
flag: described(choice('dangerous_three'), 'Keep only agents that carry this boundary flag with status exposed, mitigated or unknown.'),
|
|
74
|
+
flagStatus: described(choice('exposed', 'mitigated', 'unknown', 'absent'), 'Keep only agents whose dangerous-three flag has this status; `absent` cannot be combined with `flag`.'),
|
|
75
|
+
includeFlags: described({ type: 'boolean' }, 'Add the dangerous-three status to every returned agent. It reads tasks, so it costs more than the plain list.'),
|
|
76
|
+
limit: limit('agents'),
|
|
77
|
+
offset: offset('agents'),
|
|
78
|
+
}),
|
|
79
|
+
get_agent_access: schema({ sourceId: source('The source that holds the agent.'), agentId: agent('The agent to show.') }, ['sourceId', 'agentId']),
|
|
80
|
+
explain_access: schema({
|
|
81
|
+
sourceId: source('The source that holds the task.'),
|
|
82
|
+
taskId: described(idSchema, "The task to explain. Use its `id` or its `taskId` from an agent's tasks."),
|
|
83
|
+
tool: described(idSchema, 'The exact tool name, as written in the declared grant.'),
|
|
84
|
+
resourceId: described(idSchema, 'The exact resource selector, as written in the declared grant (a selector, not an ARN).'),
|
|
85
|
+
}, ['sourceId', 'taskId', 'tool', 'resourceId']),
|
|
86
|
+
find_agents_with_access: schema({
|
|
87
|
+
resourceRef: described(idSchema, 'The exact resource reference of a host binding, or the exact ARN that binding maps to.'),
|
|
88
|
+
action: described({ type: 'string', minLength: 3, maxLength: 256, pattern: '^[a-z0-9][a-z0-9-]*:[A-Za-z][A-Za-z0-9]*$' }, 'The cloud action in `service:Action` form, for example `s3:GetObject`.'),
|
|
89
|
+
sourceId: source('Search this source only.'),
|
|
90
|
+
limit: limit('candidate agents'),
|
|
91
|
+
}, ['resourceRef', 'action']),
|
|
92
|
+
get_changes: schema({
|
|
93
|
+
sourceId: source('The source whose snapshots to compare.'),
|
|
94
|
+
since: described(idSchema, 'The snapshot to compare from: `previous` (the default), or the id or snapshot id of the current or previous snapshot.'),
|
|
95
|
+
agentId: agent('Keep only changes that affect this agent.'),
|
|
96
|
+
resourceId: described(idSchema, 'Keep only changes that touch this resource binding. Use its `id` or `bindingId` from list_resources.'),
|
|
97
|
+
changeKind: described(choice('all', 'permissions', 'ownership', 'bindings', 'outside_guard', 'inventory'), 'Keep only changes of this kind; `all` (the default) keeps every change.'),
|
|
98
|
+
offset: offset('changes'),
|
|
99
|
+
limit: limit('changes'),
|
|
100
|
+
}, ['sourceId']),
|
|
101
|
+
get_inventory_health: schema({ sourceId: source('Report this source only.') }),
|
|
102
|
+
list_owners: schema({
|
|
103
|
+
sourceId: source('Keep owners of this source only.'),
|
|
104
|
+
search: described(searchSchema, 'Keep owners whose name or owner ID contains this text, ignoring case.'),
|
|
105
|
+
kind: described(choice('person', 'team', 'service'), 'Keep owners of this kind.'),
|
|
106
|
+
offset: offset('owners'),
|
|
107
|
+
limit: limit('owners'),
|
|
108
|
+
}),
|
|
109
|
+
list_resources: schema({
|
|
110
|
+
sourceId: source('Keep resources of this source only.'),
|
|
111
|
+
search: described(searchSchema, 'Keep resources whose display name, owner ID, binding ID or resource reference contains this text, ignoring case.'),
|
|
112
|
+
ownerId: described(idSchema, 'Keep resources whose declared owner has exactly this owner ID.'),
|
|
113
|
+
sensitivity: described(choice('public', 'internal', 'confidential', 'restricted', 'unknown'), 'Keep resources with this declared sensitivity.'),
|
|
114
|
+
offset: offset('resources'),
|
|
115
|
+
limit: limit('resources'),
|
|
116
|
+
}),
|
|
117
|
+
get_resource_access: schema({
|
|
118
|
+
sourceId: source('The source that holds the resource.'),
|
|
119
|
+
resourceId: described(idSchema, 'The resource binding to show. Use its `id` or `bindingId` from list_resources.'),
|
|
120
|
+
offset: offset('connected agents'),
|
|
121
|
+
limit: limit('connected agents'),
|
|
122
|
+
}, ['sourceId', 'resourceId']),
|
|
123
|
+
get_agent_inventory: schema({
|
|
124
|
+
sourceId: source('The source that holds the agent.'),
|
|
125
|
+
agentId: agent('The agent whose inventory to read.'),
|
|
126
|
+
section: described(choice(...sections), 'The section to page through; `overview` (the default) returns one summary row.'),
|
|
127
|
+
offset: offset('rows of the section'),
|
|
128
|
+
limit: limit('rows of the section'),
|
|
129
|
+
}, ['sourceId', 'agentId']),
|
|
130
|
+
/** Tasks or caller principals of one source that no declared agent accounts for; all-teams readers. */
|
|
131
|
+
list_undeclared_activity: schema({
|
|
132
|
+
sourceId: source('The source whose activity to list.'),
|
|
133
|
+
kind: described(choice('tasks', 'principals'), 'List undeclared `tasks` (the default) or the caller `principals` behind them.'),
|
|
134
|
+
offset: offset('rows'),
|
|
135
|
+
limit: limit('rows'),
|
|
136
|
+
}, ['sourceId']),
|
|
137
|
+
/** IAM roles and users of the enrolled AWS accounts that no declared agent names; all-teams readers. */
|
|
138
|
+
list_unclaimed_identities: schema({ offset: offset('identities'), limit: limit('identities') }),
|
|
139
|
+
});
|
|
140
|
+
const invalid = () => fail('MAP_REQUEST_INVALID');
|
|
141
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u;
|
|
142
|
+
/** The arguments of one operation, copied and checked against its closed schema. */
|
|
143
|
+
export function validateQueryArguments(operation, value) {
|
|
144
|
+
if (typeof operation !== 'string' || !MAP_OPERATIONS.includes(operation))
|
|
145
|
+
invalid();
|
|
146
|
+
let copied;
|
|
147
|
+
try {
|
|
148
|
+
copied = immutableJson(value, 4, 512, 64 * 1024);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return invalid();
|
|
152
|
+
}
|
|
153
|
+
if (!copied || typeof copied !== 'object' || Array.isArray(copied))
|
|
154
|
+
invalid();
|
|
155
|
+
const args = copied;
|
|
156
|
+
const definition = MAP_INPUT_SCHEMAS[operation];
|
|
157
|
+
const properties = definition.properties;
|
|
158
|
+
const required = definition.required;
|
|
159
|
+
if (Object.keys(args).some(key => !Object.hasOwn(properties, key)) || required.some(key => !Object.hasOwn(args, key)))
|
|
160
|
+
invalid();
|
|
161
|
+
for (const [key, item] of Object.entries(args)) {
|
|
162
|
+
if (key === 'limit' || key === 'offset') {
|
|
163
|
+
if (typeof item !== 'number' ||
|
|
164
|
+
!Number.isSafeInteger(item) ||
|
|
165
|
+
item < (key === 'offset' ? 0 : 1) ||
|
|
166
|
+
item > (key === 'offset' ? 100_000 : 100))
|
|
167
|
+
invalid();
|
|
168
|
+
}
|
|
169
|
+
else if (properties[key].type === 'boolean') {
|
|
170
|
+
if (typeof item !== 'boolean')
|
|
171
|
+
invalid();
|
|
172
|
+
}
|
|
173
|
+
else if (key === 'search') {
|
|
174
|
+
if (typeof item !== 'string' || [...item].length > 256 || item.includes('\0') || LONE_SURROGATE.test(item))
|
|
175
|
+
invalid();
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
mapIdentifier(item);
|
|
179
|
+
const allowed = properties[key].enum;
|
|
180
|
+
if (Array.isArray(allowed) && !allowed.includes(item))
|
|
181
|
+
invalid();
|
|
182
|
+
if (key === 'action' &&
|
|
183
|
+
(typeof item !== 'string' || item.length > 256 || !/^[a-z0-9][a-z0-9-]*:[A-Za-z][A-Za-z0-9]*$/.test(item)))
|
|
184
|
+
invalid();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return args;
|
|
188
|
+
}
|
|
189
|
+
/** The page an operation was asked for; 50 rows from the start unless the caller says otherwise. */
|
|
190
|
+
export function pageArguments(args) {
|
|
191
|
+
return { offset: args.offset ?? 0, limit: args.limit ?? 50 };
|
|
192
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { JsonObject, MapEdge, MapNode, MapProjection, MapSource, MapView } from '../types.js';
|
|
2
|
+
import { QueryWork } from './work.js';
|
|
3
|
+
export declare function hostObservation(node: MapNode): JsonObject;
|
|
4
|
+
export declare function deployments(projection: MapProjection, agent: MapNode, work: QueryWork): MapNode[];
|
|
5
|
+
export declare function agentSummary(source: MapSource, projection: MapProjection, agent: MapNode, work: QueryWork): JsonObject;
|
|
6
|
+
export declare function taskSummary(node: MapNode): JsonObject;
|
|
7
|
+
export declare function grantSummary(node: MapNode): JsonObject;
|
|
8
|
+
export declare function receipt(source: MapSource, projection?: MapProjection): JsonObject;
|
|
9
|
+
export declare function findSource(view: MapView, id: string): MapSource;
|
|
10
|
+
export declare function findAgent(projection: MapProjection, id: string): MapNode;
|
|
11
|
+
/** Keep the selected agent's neighborhood explicit; never expand through shared tools to other agents. */
|
|
12
|
+
/** An agent's own nodes and the edges among them, in projection order. */
|
|
13
|
+
export interface FocusedGraph {
|
|
14
|
+
nodes: MapNode[];
|
|
15
|
+
edges: MapEdge[];
|
|
16
|
+
}
|
|
17
|
+
export declare function focus(projection: MapProjection, agent: MapNode, work: QueryWork): FocusedGraph;
|
|
18
|
+
export declare function freshness(observedAt: unknown, now: number, staleSeconds: number): JsonObject;
|
|
19
|
+
export declare function changeSummary(node: MapNode): JsonObject;
|
|
20
|
+
export declare const collection: Readonly<{
|
|
21
|
+
approvals: "not_collected";
|
|
22
|
+
requestTimeDecisions: "not_collected";
|
|
23
|
+
backendReceipts: "not_collected";
|
|
24
|
+
externalPolicyHistory: "not_collected";
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* `counts` only over a whole scoped projection (list_owners). Over one agent's neighborhood
|
|
28
|
+
* they would count that agent alone, so the agent inventory leaves them out.
|
|
29
|
+
*/
|
|
30
|
+
export declare function ownerSummary(source: MapSource, projection: MapProjection, node: MapNode, work: QueryWork, counts?: boolean): JsonObject;
|
|
31
|
+
export declare function resourceSummary(source: MapSource, projection: MapProjection, node: MapNode, work: QueryWork): JsonObject;
|
|
32
|
+
export declare function relationshipRows(projection: MapProjection, work: QueryWork): JsonObject[];
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { fail } from '../common.js';
|
|
2
|
+
import { safeProperties } from '../projection-fields.js';
|
|
3
|
+
import { jsonObject } from './json.js';
|
|
4
|
+
import { ordered } from './ordering.js';
|
|
5
|
+
import { connected, matching, nodeIds, number, QueryWork, string, strings, unique } from './work.js';
|
|
6
|
+
export function hostObservation(node) {
|
|
7
|
+
return {
|
|
8
|
+
authorityId: string(node, 'authority_id'),
|
|
9
|
+
manifestId: string(node, 'manifest_id'),
|
|
10
|
+
manifestRevision: number(node, 'manifest_revision'),
|
|
11
|
+
observedAt: string(node, 'manifest_observed_at'),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function deployments(projection, agent, work) {
|
|
15
|
+
return connected(projection, new Set([agent.id]), 'G_INSTANCE_OF', work, true).filter(node => node.label === 'GuardDeployment');
|
|
16
|
+
}
|
|
17
|
+
export function agentSummary(source, projection, agent, work) {
|
|
18
|
+
const items = deployments(projection, agent, work).map(node => ({
|
|
19
|
+
id: node.entityId,
|
|
20
|
+
deploymentId: string(node, 'deployment_id'),
|
|
21
|
+
environment: string(node, 'environment'),
|
|
22
|
+
}));
|
|
23
|
+
return {
|
|
24
|
+
id: agent.entityId,
|
|
25
|
+
agentId: string(agent, 'agent_id'),
|
|
26
|
+
sourceId: source.id,
|
|
27
|
+
name: string(agent, 'name'),
|
|
28
|
+
ownerId: string(agent, 'owner_id'),
|
|
29
|
+
teamId: string(agent, 'team_id'),
|
|
30
|
+
purpose: string(agent, 'purpose'),
|
|
31
|
+
lifecycleState: string(agent, 'lifecycle_state') ?? 'unknown',
|
|
32
|
+
ownershipStatus: string(agent, 'ownership_status') ?? 'unknown',
|
|
33
|
+
ownershipSource: string(agent, 'ownership_source'),
|
|
34
|
+
ownerName: connected(projection, new Set([agent.id]), 'G_OWNED_BY', work)
|
|
35
|
+
.filter(n => n.label === 'GuardOwner')
|
|
36
|
+
.map(n => string(n, 'name'))[0] ?? null,
|
|
37
|
+
ownershipObservedAt: string(agent, 'manifest_observed_at'),
|
|
38
|
+
...hostObservation(agent),
|
|
39
|
+
environments: unique(items.map(item => item.environment).filter((item) => item !== null)).sort(),
|
|
40
|
+
deployments: items,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function taskSummary(node) {
|
|
44
|
+
return {
|
|
45
|
+
id: node.entityId,
|
|
46
|
+
taskId: string(node, 'task_id'),
|
|
47
|
+
principalId: string(node, 'principal_id'),
|
|
48
|
+
revoked: node.properties.revoked ?? null,
|
|
49
|
+
sensitive: node.properties.sensitive ?? null,
|
|
50
|
+
usage: {
|
|
51
|
+
calls: number(node, 'calls'),
|
|
52
|
+
concurrent: number(node, 'concurrent'),
|
|
53
|
+
spent: number(node, 'spent'),
|
|
54
|
+
reserved: number(node, 'reserved'),
|
|
55
|
+
},
|
|
56
|
+
budget: {
|
|
57
|
+
maxCalls: number(node, 'max_calls'),
|
|
58
|
+
maxConcurrent: number(node, 'max_concurrent'),
|
|
59
|
+
maxCost: number(node, 'max_cost'),
|
|
60
|
+
deadline: number(node, 'deadline'),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function grantSummary(node) {
|
|
65
|
+
return {
|
|
66
|
+
id: node.entityId,
|
|
67
|
+
taskId: string(node, 'task_id'),
|
|
68
|
+
tool: string(node, 'tool'),
|
|
69
|
+
resources: strings(node, 'resources'),
|
|
70
|
+
evidenceKind: 'declared',
|
|
71
|
+
assessment: 'unknown',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function receipt(source, projection = source.current) {
|
|
75
|
+
return {
|
|
76
|
+
sourceId: source.id,
|
|
77
|
+
tenantId: source.tenantId,
|
|
78
|
+
profile: source.profile,
|
|
79
|
+
projectionId: projection.id,
|
|
80
|
+
snapshotId: projection.snapshotId,
|
|
81
|
+
generatedAt: projection.generatedAt,
|
|
82
|
+
coverage: jsonObject(projection.coverage),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function findSource(view, id) {
|
|
86
|
+
const source = view.sources.find(item => item.id === id);
|
|
87
|
+
if (!source)
|
|
88
|
+
fail('MAP_NOT_FOUND');
|
|
89
|
+
return source;
|
|
90
|
+
}
|
|
91
|
+
export function findAgent(projection, id) {
|
|
92
|
+
const agent = projection.nodes.find(node => node.label === 'GuardAgent' && matching(node, id, 'agent_id'));
|
|
93
|
+
if (!agent)
|
|
94
|
+
fail('MAP_NOT_FOUND');
|
|
95
|
+
return agent;
|
|
96
|
+
}
|
|
97
|
+
export function focus(projection, agent, work) {
|
|
98
|
+
const deployment = deployments(projection, agent, work);
|
|
99
|
+
const tasks = connected(projection, nodeIds(deployment), 'G_ASSIGNED_TO', work, true);
|
|
100
|
+
const executions = connected(projection, nodeIds(deployment), 'G_EXECUTES_FOR', work, true);
|
|
101
|
+
const grants = connected(projection, nodeIds(tasks), 'G_HAS_GRANT', work);
|
|
102
|
+
const resources = connected(projection, nodeIds(grants), 'G_TARGETS', work);
|
|
103
|
+
const resourceBindings = connected(projection, nodeIds(resources), 'G_BINDS_SELECTOR', work, true);
|
|
104
|
+
const tools = [
|
|
105
|
+
...connected(projection, nodeIds(grants), 'G_FOR_TOOL', work),
|
|
106
|
+
...connected(projection, nodeIds(executions), 'G_EXECUTES_TOOL', work),
|
|
107
|
+
];
|
|
108
|
+
const connections = connected(projection, nodeIds(executions), 'G_VIA_CONNECTION', work), toolIds = nodeIds(tools);
|
|
109
|
+
const operations = connected(projection, nodeIds(connections), 'G_EXPOSES_OPERATION', work).filter(operation => connected(projection, new Set([operation.id]), 'G_OPERATION_TOOL', work).some(tool => toolIds.has(tool.id)));
|
|
110
|
+
const selected = nodeIds([
|
|
111
|
+
agent,
|
|
112
|
+
...deployment,
|
|
113
|
+
...tasks,
|
|
114
|
+
...executions,
|
|
115
|
+
...grants,
|
|
116
|
+
...resources,
|
|
117
|
+
...resourceBindings,
|
|
118
|
+
...tools,
|
|
119
|
+
...connections,
|
|
120
|
+
...operations,
|
|
121
|
+
...connected(projection, nodeIds(tasks), 'G_ACTS_AS', work),
|
|
122
|
+
...connected(projection, new Set([agent.id]), 'G_OWNED_BY', work),
|
|
123
|
+
...connected(projection, nodeIds(resourceBindings), 'G_RESOURCE_OWNED_BY', work),
|
|
124
|
+
...connected(projection, nodeIds(deployment), 'G_USES_CREDENTIAL', work),
|
|
125
|
+
...connected(projection, nodeIds(deployment), 'G_DIRECT_ACCESS_FOR', work, true),
|
|
126
|
+
...connected(projection, nodeIds(executions), 'G_USES_IDENTITY', work),
|
|
127
|
+
...connected(projection, nodeIds(resourceBindings), 'G_MAPS_TO', work),
|
|
128
|
+
]);
|
|
129
|
+
// Build the subgraph from the index instead of scanning the whole projection per agent:
|
|
130
|
+
// the same nodes and edges in projection order, at a cost proportional to this subgraph.
|
|
131
|
+
const index = work.index(projection), nodePositions = [], edgePositions = [];
|
|
132
|
+
for (const id of selected) {
|
|
133
|
+
nodePositions.push(...(index.nodePositions.get(id) ?? []));
|
|
134
|
+
const outgoing = index.edgesFrom.get(id) ?? [];
|
|
135
|
+
work.step(1 + outgoing.length);
|
|
136
|
+
for (const position of outgoing)
|
|
137
|
+
if (selected.has(projection.edges[position].target))
|
|
138
|
+
edgePositions.push(position);
|
|
139
|
+
}
|
|
140
|
+
nodePositions.sort((a, b) => a - b);
|
|
141
|
+
edgePositions.sort((a, b) => a - b);
|
|
142
|
+
return {
|
|
143
|
+
nodes: nodePositions.map(position => projection.nodes[position]),
|
|
144
|
+
edges: edgePositions.map(position => projection.edges[position]),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function freshness(observedAt, now, staleSeconds) {
|
|
148
|
+
const captured = typeof observedAt === 'string' ? Date.parse(observedAt) : NaN, age = now - captured;
|
|
149
|
+
return {
|
|
150
|
+
stale: !Number.isFinite(captured) || age < -60_000 || age > staleSeconds * 1000,
|
|
151
|
+
ageSeconds: Number.isFinite(captured) ? Math.max(0, Math.floor(age / 1000)) : null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function changeSummary(node) {
|
|
155
|
+
return {
|
|
156
|
+
entityId: node.entityId,
|
|
157
|
+
label: node.label,
|
|
158
|
+
name: string(node, 'name') ??
|
|
159
|
+
string(node, 'display_name') ??
|
|
160
|
+
string(node, 'description') ??
|
|
161
|
+
string(node, 'task_id') ??
|
|
162
|
+
string(node, 'agent_id') ??
|
|
163
|
+
string(node, 'deployment_id') ??
|
|
164
|
+
string(node, 'binding_id') ??
|
|
165
|
+
string(node, 'connection_id') ??
|
|
166
|
+
string(node, 'resource_id') ??
|
|
167
|
+
string(node, 'tool') ??
|
|
168
|
+
node.entityId,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export const collection = Object.freeze({
|
|
172
|
+
approvals: 'not_collected',
|
|
173
|
+
requestTimeDecisions: 'not_collected',
|
|
174
|
+
backendReceipts: 'not_collected',
|
|
175
|
+
externalPolicyHistory: 'not_collected',
|
|
176
|
+
});
|
|
177
|
+
/**
|
|
178
|
+
* `counts` only over a whole scoped projection (list_owners). Over one agent's neighborhood
|
|
179
|
+
* they would count that agent alone, so the agent inventory leaves them out.
|
|
180
|
+
*/
|
|
181
|
+
export function ownerSummary(source, projection, node, work, counts = true) {
|
|
182
|
+
return {
|
|
183
|
+
id: node.entityId,
|
|
184
|
+
ownerId: string(node, 'owner_id'),
|
|
185
|
+
name: string(node, 'name'),
|
|
186
|
+
kind: string(node, 'kind'),
|
|
187
|
+
sourceId: source.id,
|
|
188
|
+
...hostObservation(node),
|
|
189
|
+
...(counts
|
|
190
|
+
? {
|
|
191
|
+
agentCount: connected(projection, new Set([node.id]), 'G_OWNED_BY', work, true).filter(n => n.label === 'GuardAgent').length,
|
|
192
|
+
resourceCount: connected(projection, new Set([node.id]), 'G_RESOURCE_OWNED_BY', work, true).filter(n => n.label === 'GuardResourceBinding').length,
|
|
193
|
+
}
|
|
194
|
+
: {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export function resourceSummary(source, projection, node, work) {
|
|
198
|
+
return {
|
|
199
|
+
id: node.entityId,
|
|
200
|
+
bindingId: string(node, 'binding_id'),
|
|
201
|
+
sourceId: source.id,
|
|
202
|
+
displayName: string(node, 'display_name') ?? string(node, 'resource_ref'),
|
|
203
|
+
tool: string(node, 'tool'),
|
|
204
|
+
resourceRef: string(node, 'resource_ref'),
|
|
205
|
+
scope: string(node, 'scope'),
|
|
206
|
+
actions: strings(node, 'actions'),
|
|
207
|
+
ownerId: string(node, 'owner_id'),
|
|
208
|
+
ownerName: connected(projection, new Set([node.id]), 'G_RESOURCE_OWNED_BY', work)
|
|
209
|
+
.filter(n => n.label === 'GuardOwner')
|
|
210
|
+
.map(n => string(n, 'name'))[0] ?? null,
|
|
211
|
+
ownershipStatus: string(node, 'ownership_status') ?? 'unknown',
|
|
212
|
+
ownershipSource: string(node, 'ownership_source'),
|
|
213
|
+
sensitivity: string(node, 'sensitivity') ?? 'unknown',
|
|
214
|
+
approvedDestinations: strings(node, 'approved_destinations'),
|
|
215
|
+
targets: connected(projection, new Set([node.id]), 'G_MAPS_TO', work)
|
|
216
|
+
.filter(n => n.label === 'GuardExternalResource')
|
|
217
|
+
.map(n => ({ id: n.entityId, ...safeProperties(n) })),
|
|
218
|
+
dependencies: connected(projection, new Set([node.id]), 'G_DEPENDS_ON', work)
|
|
219
|
+
.filter(n => n.label === 'GuardResourceBinding')
|
|
220
|
+
.map(n => ({
|
|
221
|
+
id: n.entityId,
|
|
222
|
+
bindingId: string(n, 'binding_id'),
|
|
223
|
+
displayName: string(n, 'display_name') ?? string(n, 'resource_ref'),
|
|
224
|
+
})),
|
|
225
|
+
...hostObservation(node),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
export function relationshipRows(projection, work) {
|
|
229
|
+
const index = work.index(projection);
|
|
230
|
+
work.step(projection.edges.length);
|
|
231
|
+
return ordered(projection.edges.flatMap(edge => {
|
|
232
|
+
const source = index.nodes.get(edge.source), target = index.nodes.get(edge.target);
|
|
233
|
+
return source && target
|
|
234
|
+
? [
|
|
235
|
+
{
|
|
236
|
+
source: source.entityId,
|
|
237
|
+
target: target.entityId,
|
|
238
|
+
kind: edge.kind,
|
|
239
|
+
sourceName: changeSummary(source).name,
|
|
240
|
+
sourceLabel: source.label,
|
|
241
|
+
targetName: changeSummary(target).name,
|
|
242
|
+
targetLabel: target.label,
|
|
243
|
+
},
|
|
244
|
+
]
|
|
245
|
+
: [];
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type GraphIndex } from '../common.js';
|
|
2
|
+
import type { JsonObject, MapEdge, MapNode, MapProjection } from '../types.js';
|
|
3
|
+
export declare const string: (node: MapNode, key: string) => string | null;
|
|
4
|
+
export declare const number: (node: MapNode, key: string) => number | null;
|
|
5
|
+
export declare const strings: (node: MapNode, key: string) => readonly string[];
|
|
6
|
+
export declare const matching: (node: MapNode, id: string, native: string) => boolean;
|
|
7
|
+
export declare const unique: <T>(values: readonly T[]) => T[];
|
|
8
|
+
export interface ProjectionLookup {
|
|
9
|
+
/** The first tool node of each name. */
|
|
10
|
+
readonly tools: ReadonlyMap<string, MapNode>;
|
|
11
|
+
/** Resource bindings by `bindingKey(tool, resource_ref)`. */
|
|
12
|
+
readonly bindings: ReadonlyMap<string, readonly MapNode[]>;
|
|
13
|
+
}
|
|
14
|
+
export declare const bindingKey: (tool: string, reference: string) => string;
|
|
15
|
+
/** Count authorized work and elapsed monotonic time, including synchronous projection. */
|
|
16
|
+
export declare class QueryWork {
|
|
17
|
+
#private;
|
|
18
|
+
constructor(timeout: number, maxBytes: number);
|
|
19
|
+
/** Milliseconds left before this query's deadline. */
|
|
20
|
+
remaining(): number;
|
|
21
|
+
step(amount?: number): void;
|
|
22
|
+
index(projection: MapProjection): GraphIndex;
|
|
23
|
+
/** A value derived from one projection, built once per query (for example its validated policy evidence). */
|
|
24
|
+
cached<T>(projection: MapProjection, name: string, build: () => T): T;
|
|
25
|
+
evidence(value: JsonObject): JsonObject;
|
|
26
|
+
/**
|
|
27
|
+
* Tools by name and resource bindings by tool and reference, in projection order. Built once
|
|
28
|
+
* per projection, so a row of evidence costs its own lookups and not a scan of every node.
|
|
29
|
+
*/
|
|
30
|
+
lookup(projection: MapProjection): ProjectionLookup;
|
|
31
|
+
}
|
|
32
|
+
export declare function connected(projection: MapProjection, ids: Set<string>, kind: MapEdge['kind'], work: QueryWork, reverse?: boolean): MapNode[];
|
|
33
|
+
export declare const nodeIds: (nodes: readonly MapNode[]) => Set<string>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { immutableJson } from '@shieldaai/agent-guard/internal';
|
|
3
|
+
import { buildGraphIndex, fail } from '../common.js';
|
|
4
|
+
export const string = (node, key) => typeof node.properties[key] === 'string' ? node.properties[key] : null;
|
|
5
|
+
export const number = (node, key) => typeof node.properties[key] === 'number' ? node.properties[key] : null;
|
|
6
|
+
export const strings = (node, key) => Array.isArray(node.properties[key])
|
|
7
|
+
? node.properties[key].filter((v) => typeof v === 'string')
|
|
8
|
+
: [];
|
|
9
|
+
export const matching = (node, id, native) => node.entityId === id || string(node, native) === id;
|
|
10
|
+
export const unique = (values) => [...new Set(values)];
|
|
11
|
+
export const bindingKey = (tool, reference) => JSON.stringify([tool, reference]);
|
|
12
|
+
/** Count authorized work and elapsed monotonic time, including synchronous projection. */
|
|
13
|
+
export class QueryWork {
|
|
14
|
+
#deadline;
|
|
15
|
+
#indices = new WeakMap();
|
|
16
|
+
#derived = new WeakMap();
|
|
17
|
+
#lookups = new WeakMap();
|
|
18
|
+
#remaining = 200_000;
|
|
19
|
+
#evidenceBytes;
|
|
20
|
+
#evidenceCount = 0;
|
|
21
|
+
constructor(timeout, maxBytes) {
|
|
22
|
+
this.#deadline = performance.now() + timeout;
|
|
23
|
+
this.#evidenceBytes = maxBytes;
|
|
24
|
+
}
|
|
25
|
+
/** Milliseconds left before this query's deadline. */
|
|
26
|
+
remaining() {
|
|
27
|
+
return Math.max(0, this.#deadline - performance.now());
|
|
28
|
+
}
|
|
29
|
+
step(amount = 1) {
|
|
30
|
+
if (performance.now() >= this.#deadline)
|
|
31
|
+
fail('MAP_TIMEOUT');
|
|
32
|
+
this.#remaining -= amount;
|
|
33
|
+
if (this.#remaining < 0)
|
|
34
|
+
fail('MAP_LIMIT_EXCEEDED');
|
|
35
|
+
}
|
|
36
|
+
index(projection) {
|
|
37
|
+
this.step();
|
|
38
|
+
const existing = this.#indices.get(projection);
|
|
39
|
+
if (existing)
|
|
40
|
+
return existing;
|
|
41
|
+
this.step(projection.nodes.length + projection.edges.length);
|
|
42
|
+
const result = buildGraphIndex(projection.nodes, projection.edges);
|
|
43
|
+
this.#indices.set(projection, result);
|
|
44
|
+
this.step();
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
/** A value derived from one projection, built once per query (for example its validated policy evidence). */
|
|
48
|
+
cached(projection, name, build) {
|
|
49
|
+
let values = this.#derived.get(projection);
|
|
50
|
+
if (!values) {
|
|
51
|
+
values = new Map();
|
|
52
|
+
this.#derived.set(projection, values);
|
|
53
|
+
}
|
|
54
|
+
if (!values.has(name))
|
|
55
|
+
values.set(name, build());
|
|
56
|
+
return values.get(name);
|
|
57
|
+
}
|
|
58
|
+
evidence(value) {
|
|
59
|
+
this.step();
|
|
60
|
+
if (++this.#evidenceCount > 1000)
|
|
61
|
+
fail('MAP_LIMIT_EXCEEDED');
|
|
62
|
+
let copy;
|
|
63
|
+
try {
|
|
64
|
+
copy = immutableJson(value, 24, 25_000, this.#evidenceBytes);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return fail('MAP_LIMIT_EXCEEDED');
|
|
68
|
+
}
|
|
69
|
+
this.#evidenceBytes -= Buffer.byteLength(JSON.stringify(copy));
|
|
70
|
+
this.step();
|
|
71
|
+
return copy;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Tools by name and resource bindings by tool and reference, in projection order. Built once
|
|
75
|
+
* per projection, so a row of evidence costs its own lookups and not a scan of every node.
|
|
76
|
+
*/
|
|
77
|
+
lookup(projection) {
|
|
78
|
+
const existing = this.#lookups.get(projection);
|
|
79
|
+
if (existing)
|
|
80
|
+
return existing;
|
|
81
|
+
this.step(projection.nodes.length);
|
|
82
|
+
const tools = new Map(), bindings = new Map();
|
|
83
|
+
for (const node of projection.nodes) {
|
|
84
|
+
if (node.label === 'GuardTool') {
|
|
85
|
+
const name = string(node, 'name');
|
|
86
|
+
if (name !== null && !tools.has(name))
|
|
87
|
+
tools.set(name, node);
|
|
88
|
+
}
|
|
89
|
+
else if (node.label === 'GuardResourceBinding') {
|
|
90
|
+
const tool = string(node, 'tool'), reference = string(node, 'resource_ref');
|
|
91
|
+
if (tool === null || reference === null)
|
|
92
|
+
continue;
|
|
93
|
+
const key = bindingKey(tool, reference), items = bindings.get(key);
|
|
94
|
+
if (items)
|
|
95
|
+
items.push(node);
|
|
96
|
+
else
|
|
97
|
+
bindings.set(key, [node]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const result = { tools, bindings };
|
|
101
|
+
this.#lookups.set(projection, result);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function connected(projection, ids, kind, work, reverse = false) {
|
|
106
|
+
const index = work.index(projection), selected = new Set();
|
|
107
|
+
work.step(ids.size);
|
|
108
|
+
for (const id of ids) {
|
|
109
|
+
const targets = (reverse ? index.incoming : index.outgoing).get(id)?.get(kind) ?? [];
|
|
110
|
+
work.step(targets.length);
|
|
111
|
+
for (const target of targets)
|
|
112
|
+
selected.add(target);
|
|
113
|
+
}
|
|
114
|
+
return [...selected].flatMap(id => (index.nodes.get(id) ? [index.nodes.get(id)] : []));
|
|
115
|
+
}
|
|
116
|
+
export const nodeIds = (nodes) => new Set(nodes.map(node => node.id));
|
package/dist/service.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type JsonObject, type
|
|
2
|
-
|
|
3
|
-
export type MapOperation
|
|
4
|
-
export
|
|
1
|
+
import { type JsonObject, type MapEdge, type MapNode, type MapStore, type ReadScope } from './types.js';
|
|
2
|
+
import { type MapOperation } from './service/schemas.js';
|
|
3
|
+
export { MAP_OPERATIONS, MAP_INPUT_SCHEMAS, MAP_OPERATION_DESCRIPTIONS, validateQueryArguments, type MapOperation, } from './service/schemas.js';
|
|
4
|
+
export { MAP_LABELS, MAP_RELATIONSHIPS } from './types.js';
|
|
5
|
+
export { MAP_PROPERTIES } from './projection-fields.js';
|
|
5
6
|
export interface MapResponse {
|
|
6
7
|
readonly operation: MapOperation;
|
|
7
8
|
readonly observedAt: string;
|
|
@@ -19,7 +20,6 @@ export interface MapServiceOptions {
|
|
|
19
20
|
readonly queryTimeoutMs?: number;
|
|
20
21
|
readonly staleAfterSeconds?: number;
|
|
21
22
|
}
|
|
22
|
-
export declare function validateQueryArguments(operation: unknown, value: unknown): Readonly<Record<string, JsonValue>>;
|
|
23
23
|
/** One read-only evidence layer shared by HTTP, MCP and the browser. */
|
|
24
24
|
export declare class MapService {
|
|
25
25
|
#private;
|