@shieldaai/agent-map 0.1.0-alpha.3 → 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 +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
package/dist/transport.js
CHANGED
|
@@ -1,16 +1,50 @@
|
|
|
1
1
|
import { MapQueryError } from './types.js';
|
|
2
|
-
import { fail } from './
|
|
2
|
+
import { fail, integer } from './common.js';
|
|
3
|
+
import { fields, parseRequest } from './boundary.js';
|
|
4
|
+
// Request plumbing shared by the reference-credential server (http.ts) and the company
|
|
5
|
+
// sign-in server (identity-http.ts): the same headers, limits, error bodies and refusals.
|
|
3
6
|
const CODES = new Map([
|
|
4
|
-
['MAP_UNAUTHENTICATED', 401],
|
|
5
|
-
['
|
|
6
|
-
['
|
|
7
|
-
['
|
|
8
|
-
['
|
|
9
|
-
['
|
|
7
|
+
['MAP_UNAUTHENTICATED', 401],
|
|
8
|
+
['MAP_REQUEST_DENIED', 403],
|
|
9
|
+
['MAP_NOT_FOUND', 404],
|
|
10
|
+
['MAP_REQUEST_INVALID', 400],
|
|
11
|
+
['MAP_SCOPE_INVALID', 403],
|
|
12
|
+
['MAP_LIMIT_EXCEEDED', 413],
|
|
13
|
+
['MAP_REQUEST_TOO_LARGE', 413],
|
|
14
|
+
['MAP_RESPONSE_TOO_LARGE', 413],
|
|
15
|
+
// 408 while the request is still arriving; 504 once it has (see `timeoutStatus`).
|
|
16
|
+
['MAP_TIMEOUT', 408],
|
|
17
|
+
['MAP_BUSY', 503],
|
|
18
|
+
['MAP_RATE_LIMITED', 429],
|
|
19
|
+
['MAP_UNAVAILABLE', 503],
|
|
20
|
+
['MAP_SESSION_REQUIRED', 401],
|
|
21
|
+
['MAP_IDENTITY_INVALID', 401],
|
|
22
|
+
['MAP_IDENTITY_UNAVAILABLE', 503],
|
|
10
23
|
]);
|
|
11
24
|
export function safeError(error) {
|
|
12
25
|
return error instanceof MapQueryError && CODES.has(error.code) ? error.code : 'MAP_UNAVAILABLE';
|
|
13
26
|
}
|
|
27
|
+
/** A refusal HTTP names more exactly than the code's usual status (an unmet `Expect` is 417). */
|
|
28
|
+
class StatusError extends MapQueryError {
|
|
29
|
+
status;
|
|
30
|
+
constructor(code, status) {
|
|
31
|
+
super(code);
|
|
32
|
+
this.status = status;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The status of a timeout. 408 says the client was too slow to send its request; once the whole
|
|
37
|
+
* request has been received, the time was spent by the service or its database, which is 504.
|
|
38
|
+
*/
|
|
39
|
+
export const timeoutStatus = (received) => (received ? 504 : undefined);
|
|
40
|
+
/**
|
|
41
|
+
* Answers a failed request with its safe code; nothing from the error itself is sent.
|
|
42
|
+
* `received`: the request body had been read in full when the failure happened.
|
|
43
|
+
*/
|
|
44
|
+
export function respondError(response, error, received = false) {
|
|
45
|
+
const code = safeError(error);
|
|
46
|
+
errorResponse(response, code, error instanceof StatusError ? error.status : code === 'MAP_TIMEOUT' ? timeoutStatus(received) : undefined);
|
|
47
|
+
}
|
|
14
48
|
export function headers(response, size, mime = 'application/json') {
|
|
15
49
|
response.setHeader('Content-Type', mime);
|
|
16
50
|
response.setHeader('Content-Length', size);
|
|
@@ -20,7 +54,8 @@ export function headers(response, size, mime = 'application/json') {
|
|
|
20
54
|
response.setHeader('Connection', 'close');
|
|
21
55
|
response.setHeader('Content-Security-Policy', "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; frame-ancestors 'none'; form-action 'none'");
|
|
22
56
|
}
|
|
23
|
-
|
|
57
|
+
/** `status` overrides the code's usual status, for a refusal HTTP names more exactly (417). */
|
|
58
|
+
export function errorResponse(response, code, status) {
|
|
24
59
|
if (response.writableEnded || response.destroyed)
|
|
25
60
|
return;
|
|
26
61
|
if (response.headersSent) {
|
|
@@ -31,7 +66,7 @@ export function errorResponse(response, code) {
|
|
|
31
66
|
headers(response, body.length);
|
|
32
67
|
if (code === 'MAP_UNAUTHENTICATED')
|
|
33
68
|
response.setHeader('WWW-Authenticate', 'Bearer realm="agent-guard-reference"');
|
|
34
|
-
response.writeHead(CODES.get(code) ?? 503);
|
|
69
|
+
response.writeHead(status ?? CODES.get(code) ?? 503);
|
|
35
70
|
response.end(body);
|
|
36
71
|
}
|
|
37
72
|
export const count = (request, name) => request.rawHeaders.filter((entry, i) => i % 2 === 0 && entry.toLowerCase() === name).length;
|
|
@@ -48,13 +83,30 @@ export function readBody(request, max, signal) {
|
|
|
48
83
|
return new Promise((resolve, reject) => {
|
|
49
84
|
let size = 0;
|
|
50
85
|
const chunks = [];
|
|
51
|
-
const cleanup = () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
86
|
+
const cleanup = () => {
|
|
87
|
+
request.off('data', data);
|
|
88
|
+
request.off('end', end);
|
|
89
|
+
request.off('error', aborted);
|
|
90
|
+
request.off('aborted', aborted);
|
|
91
|
+
signal.removeEventListener('abort', timeout);
|
|
92
|
+
};
|
|
93
|
+
const stop = (code) => {
|
|
94
|
+
cleanup();
|
|
95
|
+
request.pause();
|
|
96
|
+
reject(new MapQueryError(code));
|
|
97
|
+
};
|
|
98
|
+
const data = (chunk) => {
|
|
99
|
+
size += chunk.length;
|
|
100
|
+
if (size > max) {
|
|
101
|
+
stop('MAP_REQUEST_TOO_LARGE');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
chunks.push(chunk);
|
|
105
|
+
};
|
|
106
|
+
const end = () => {
|
|
107
|
+
cleanup();
|
|
108
|
+
resolve(Buffer.concat(chunks, size));
|
|
109
|
+
};
|
|
58
110
|
const aborted = () => stop('MAP_REQUEST_INVALID');
|
|
59
111
|
const timeout = () => stop('MAP_TIMEOUT');
|
|
60
112
|
if (signal.aborted) {
|
|
@@ -68,3 +120,79 @@ export function readBody(request, max, signal) {
|
|
|
68
120
|
signal.addEventListener('abort', timeout, { once: true });
|
|
69
121
|
});
|
|
70
122
|
}
|
|
123
|
+
/** Per-credential or per-session rate limit: `maxRequests` in each `windowMs`. */
|
|
124
|
+
export function rateLimit(value, defaultMaxRequests) {
|
|
125
|
+
const rate = value === undefined ? {} : fields(value, ['maxRequests', 'windowMs'], ['maxRequests', 'windowMs']);
|
|
126
|
+
return {
|
|
127
|
+
maxRequests: integer(rate.maxRequests, defaultMaxRequests, 1, 10_000),
|
|
128
|
+
windowMs: integer(rate.windowMs, 60_000, 10, 60_000),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The JSON body of an admitted POST. Call it only after the credential (or session and CSRF)
|
|
133
|
+
* and rate checks have passed: it is what tells a client that sent `Expect: 100-continue` to go
|
|
134
|
+
* on, and a refusal before it has already answered with its own status.
|
|
135
|
+
*/
|
|
136
|
+
export async function readJsonBody(request, response, limit, signal) {
|
|
137
|
+
if (count(request, 'content-type') !== 1 ||
|
|
138
|
+
!/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(request.headers['content-type'] ?? '') ||
|
|
139
|
+
count(request, 'content-length') > 1 ||
|
|
140
|
+
count(request, 'transfer-encoding') > 1 ||
|
|
141
|
+
request.headers['content-encoding'] !== undefined)
|
|
142
|
+
fail('MAP_REQUEST_INVALID');
|
|
143
|
+
const length = request.headers['content-length'];
|
|
144
|
+
if (length !== undefined && (!/^\d+$/.test(length) || !Number.isSafeInteger(Number(length))))
|
|
145
|
+
fail('MAP_REQUEST_INVALID');
|
|
146
|
+
if (length !== undefined && Number(length) > limit)
|
|
147
|
+
fail('MAP_REQUEST_TOO_LARGE');
|
|
148
|
+
const expectation = request.headers.expect;
|
|
149
|
+
if (expectation !== undefined) {
|
|
150
|
+
// Only `100-continue` can be met; anything else, or anything added to it, is 417.
|
|
151
|
+
if (expectation.trim().toLowerCase() !== '100-continue')
|
|
152
|
+
throw new StatusError('MAP_REQUEST_INVALID', 417);
|
|
153
|
+
// Interim responses exist only in HTTP/1.1. An HTTP/1.0 client that sends the expectation
|
|
154
|
+
// is not waiting for one (RFC 9110, 10.1.1): its body is read without an invitation.
|
|
155
|
+
if (request.httpVersionMajor === 1 && request.httpVersionMinor === 1)
|
|
156
|
+
response.writeContinue();
|
|
157
|
+
}
|
|
158
|
+
return parseRequest(await readBody(request, limit, signal), limit);
|
|
159
|
+
}
|
|
160
|
+
/** The `{ operation, args }` envelope of `POST /v1/query`. */
|
|
161
|
+
export function queryEnvelope(parsed) {
|
|
162
|
+
try {
|
|
163
|
+
const envelope = fields(parsed, ['operation', 'args'], ['operation', 'args']);
|
|
164
|
+
return { operation: envelope.operation, args: envelope.args };
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return fail('MAP_REQUEST_INVALID');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** A body that must be the empty object, as the sign-in and sign-out routes require. */
|
|
171
|
+
export function emptyObject(parsed) {
|
|
172
|
+
try {
|
|
173
|
+
fields(parsed, [], []);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
fail('MAP_REQUEST_INVALID');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Sends one fixed viewer asset. */
|
|
180
|
+
export function sendAsset(response, asset) {
|
|
181
|
+
headers(response, asset.body.length, asset.mime);
|
|
182
|
+
response.writeHead(200);
|
|
183
|
+
response.end(asset.body);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* What neither server speaks: a request that arrives with `Expect: 100-continue` still goes
|
|
187
|
+
* through the normal handler, any other expectation is 417, and tunnels, upgrades and
|
|
188
|
+
* malformed requests are dropped.
|
|
189
|
+
*/
|
|
190
|
+
export function refuseOtherTraffic(server, handle) {
|
|
191
|
+
server.on('checkContinue', (request, response) => {
|
|
192
|
+
void handle(request, response);
|
|
193
|
+
});
|
|
194
|
+
server.on('checkExpectation', (_request, response) => errorResponse(response, 'MAP_REQUEST_INVALID', 417));
|
|
195
|
+
server.on('connect', (_request, socket) => socket.destroy());
|
|
196
|
+
server.on('upgrade', (_request, socket) => socket.destroy());
|
|
197
|
+
server.on('clientError', (_error, socket) => socket.destroy());
|
|
198
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export interface ReadScope {
|
|
|
15
15
|
readonly teamIds: readonly string[];
|
|
16
16
|
readonly allTeams: boolean;
|
|
17
17
|
}
|
|
18
|
-
export declare const MAP_LABELS: readonly ['GuardAgent', 'GuardDeployment', 'GuardTask', 'GuardPrincipal', 'GuardGrant', 'GuardTool', 'GuardResource', 'GuardResourceBinding', 'GuardExecutionBinding', 'GuardExternalResource', 'GuardExternalIdentity', 'GuardMcpConnection', 'GuardMcpOperation'];
|
|
19
|
-
export type GuardLabel = typeof MAP_LABELS[number];
|
|
20
|
-
export declare const MAP_RELATIONSHIPS: readonly ['G_INSTANCE_OF', 'G_ASSIGNED_TO', 'G_ACTS_AS', 'G_DELEGATED_FROM', 'G_DERIVED_FROM', 'G_HAS_GRANT', 'G_FOR_TOOL', 'G_TARGETS', 'G_BINDS_SELECTOR', 'G_MAPS_TO', 'G_EXECUTES_FOR', 'G_EXECUTES_TOOL', 'G_USES_IDENTITY', 'G_VIA_CONNECTION', 'G_EXPOSES_OPERATION', 'G_OPERATION_TOOL'];
|
|
21
|
-
export type GuardRelation = typeof MAP_RELATIONSHIPS[number];
|
|
18
|
+
export declare const MAP_LABELS: readonly ['GuardAgent', 'GuardDeployment', 'GuardTask', 'GuardPrincipal', 'GuardGrant', 'GuardTool', 'GuardResource', 'GuardResourceBinding', 'GuardExecutionBinding', 'GuardExternalResource', 'GuardExternalIdentity', 'GuardMcpConnection', 'GuardMcpOperation', 'GuardOwner', 'GuardCredentialReference', 'GuardDirectAccess'];
|
|
19
|
+
export type GuardLabel = (typeof MAP_LABELS)[number];
|
|
20
|
+
export declare const MAP_RELATIONSHIPS: readonly ['G_INSTANCE_OF', 'G_ASSIGNED_TO', 'G_ACTS_AS', 'G_DELEGATED_FROM', 'G_DERIVED_FROM', 'G_HAS_GRANT', 'G_FOR_TOOL', 'G_TARGETS', 'G_BINDS_SELECTOR', 'G_MAPS_TO', 'G_EXECUTES_FOR', 'G_EXECUTES_TOOL', 'G_USES_IDENTITY', 'G_VIA_CONNECTION', 'G_EXPOSES_OPERATION', 'G_OPERATION_TOOL', 'G_OWNED_BY', 'G_RESOURCE_OWNED_BY', 'G_USES_CREDENTIAL', 'G_DEPENDS_ON', 'G_DIRECT_ACCESS_FOR'];
|
|
21
|
+
export type GuardRelation = (typeof MAP_RELATIONSHIPS)[number];
|
|
22
22
|
export interface MapNode {
|
|
23
23
|
readonly id: string;
|
|
24
24
|
readonly entityId: string;
|
|
@@ -58,6 +58,36 @@ export interface MapPolicyEvidence {
|
|
|
58
58
|
readonly assessment: 'unknown';
|
|
59
59
|
readonly candidates: readonly MapPolicyCandidate[];
|
|
60
60
|
}
|
|
61
|
+
/** A bucket a tool's static identity has an identity-policy candidate on, outside that tool's bindings. */
|
|
62
|
+
export interface MapIdentityExtraResource {
|
|
63
|
+
readonly resourceId: string;
|
|
64
|
+
readonly bucketName: string;
|
|
65
|
+
readonly relationship: MapPolicyCandidate['relationship'];
|
|
66
|
+
readonly hasCondition: boolean;
|
|
67
|
+
readonly conditionKeys: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
/** C2: what a tool's execution identity reaches compared with what that tool's grants bind. */
|
|
70
|
+
export interface MapIdentityScope {
|
|
71
|
+
readonly executionBindingId: string;
|
|
72
|
+
readonly deploymentId: string | null;
|
|
73
|
+
readonly tool: string | null;
|
|
74
|
+
readonly identityId: string | null;
|
|
75
|
+
/** The identity's AWS account; candidates are collected per account, so only its buckets are checked. */
|
|
76
|
+
readonly accountId: string | null;
|
|
77
|
+
readonly status: 'broader_than_grants' | 'matches_grants' | 'unknown';
|
|
78
|
+
readonly reason: Exclude<MapPolicyEvidence['reason'], 'no_candidate' | 'unsupported_action' | 'unsupported_binding'> | 'unsupported_identity' | 'not_evaluated' | 'wildcard_grant' | 'binding_narrower_than_candidate' | 'grant_lineage_unknown' | 'truncated';
|
|
79
|
+
readonly observedAt: string | null;
|
|
80
|
+
readonly boundBuckets: readonly string[];
|
|
81
|
+
readonly extraResources: readonly MapIdentityExtraResource[];
|
|
82
|
+
readonly truncated: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* `evidence_outside_visible_scope`: some extra buckets have no binding in your scope (another
|
|
85
|
+
* team's, or none). `bucket_list_shortened`: the list is cut; the status still stands.
|
|
86
|
+
*/
|
|
87
|
+
readonly notes?: readonly string[];
|
|
88
|
+
readonly evidenceKind: 'iam_identity_policy_candidate';
|
|
89
|
+
readonly assessment: 'unknown';
|
|
90
|
+
}
|
|
61
91
|
export interface MapProjection {
|
|
62
92
|
readonly id: string;
|
|
63
93
|
readonly snapshotId: string;
|
|
@@ -68,6 +98,105 @@ export interface MapProjection {
|
|
|
68
98
|
readonly coverage: MapCoverage;
|
|
69
99
|
/** Current, independently checked policy evidence; omitted for summaries/history. */
|
|
70
100
|
readonly policyEvidence?: readonly MapPolicyEvidence[];
|
|
101
|
+
/** Current identity-scope comparison per static execution binding; only for one-agent reads. */
|
|
102
|
+
readonly identityScope?: readonly MapIdentityScope[];
|
|
103
|
+
/**
|
|
104
|
+
* The same comparison limited to buckets that visible bindings map to; only for
|
|
105
|
+
* `outsideGuard` reads. `matches_grants` here covers only those buckets.
|
|
106
|
+
*/
|
|
107
|
+
readonly identityReach?: readonly MapIdentityScope[];
|
|
108
|
+
/**
|
|
109
|
+
* Outside-guard reads for a team-scoped reader: false when the projection has no
|
|
110
|
+
* deployment or task outside the reader's read, so `none_found` can stand. Never a count.
|
|
111
|
+
*/
|
|
112
|
+
readonly outsideVisibleScope?: boolean;
|
|
113
|
+
/** A boundary read had no room left for delegation ancestors and loaded none. */
|
|
114
|
+
readonly ancestorsNotLoaded?: boolean;
|
|
115
|
+
/** One-agent boundary reads for a team reader: host resource ids in its scope. */
|
|
116
|
+
readonly inScopeTargets?: readonly string[];
|
|
117
|
+
/**
|
|
118
|
+
* Boundary reads: for each tool the agent holds a `*` grant on, up to five in-scope bindings
|
|
119
|
+
* of that tool marked confidential or restricted. Read in place of loading every binding.
|
|
120
|
+
*/
|
|
121
|
+
readonly wildcardSensitivity?: Readonly<Record<string, readonly {
|
|
122
|
+
readonly id: string;
|
|
123
|
+
readonly sensitivity: string;
|
|
124
|
+
}[]>>;
|
|
125
|
+
/** `undeclared` reads only: activity in this projection that no declared agent accounts for. */
|
|
126
|
+
readonly undeclared?: MapUndeclaredActivity;
|
|
127
|
+
/**
|
|
128
|
+
* A `policy.optional` read found more task/binding/action pairs than one read evaluates and
|
|
129
|
+
* loaded no policy evidence. Its coverage is unknown, never `not_collected`.
|
|
130
|
+
*/
|
|
131
|
+
readonly policyEvidenceNotLoaded?: boolean;
|
|
132
|
+
}
|
|
133
|
+
/** A task seen through an enrolled executor that no manifest task binding assigns to a declared agent. */
|
|
134
|
+
export interface MapUndeclaredTask {
|
|
135
|
+
readonly id: string;
|
|
136
|
+
readonly taskId: string;
|
|
137
|
+
readonly principalId: string | null;
|
|
138
|
+
readonly delegated: boolean | null;
|
|
139
|
+
readonly derived: boolean | null;
|
|
140
|
+
readonly revoked: boolean | null;
|
|
141
|
+
/** Its direct parent or source task is assigned to a declared agent: likely that agent's sub-task. */
|
|
142
|
+
readonly parentDeclared: boolean;
|
|
143
|
+
/** Tools the task holds a declared grant on, at most 32. */
|
|
144
|
+
readonly tools: readonly string[];
|
|
145
|
+
readonly toolsTruncated: boolean;
|
|
146
|
+
}
|
|
147
|
+
/** A caller principal none of whose tasks is assigned to a declared agent. */
|
|
148
|
+
export interface MapUndeclaredPrincipal {
|
|
149
|
+
readonly id: string;
|
|
150
|
+
readonly principalId: string;
|
|
151
|
+
/** Its tasks in this projection, counted up to 1,000. */
|
|
152
|
+
readonly tasks: number;
|
|
153
|
+
readonly tasksCapped: boolean;
|
|
154
|
+
}
|
|
155
|
+
/** Only for `undeclared` reads (all-teams readers). Counting stops at 10,000: `*Capped` means "at least". */
|
|
156
|
+
export interface MapUndeclaredActivity {
|
|
157
|
+
readonly tasks: number;
|
|
158
|
+
readonly tasksCapped: boolean;
|
|
159
|
+
readonly principals: number;
|
|
160
|
+
readonly principalsCapped: boolean;
|
|
161
|
+
readonly page?: {
|
|
162
|
+
readonly kind: 'tasks';
|
|
163
|
+
readonly items: readonly MapUndeclaredTask[];
|
|
164
|
+
} | {
|
|
165
|
+
readonly kind: 'principals';
|
|
166
|
+
readonly items: readonly MapUndeclaredPrincipal[];
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** One enrolled AWS account's collection, judged for an `unclaimedIdentities` read. */
|
|
170
|
+
export interface MapCloudAccount {
|
|
171
|
+
readonly tenantId: string;
|
|
172
|
+
readonly partition: string;
|
|
173
|
+
readonly accountId: string;
|
|
174
|
+
readonly status: 'complete' | 'unknown';
|
|
175
|
+
readonly reason: 'collection_invalid' | 'stale' | 'changed' | 'limit_exceeded' | 'claims_limit_exceeded' | null;
|
|
176
|
+
readonly observedAt: string | null;
|
|
177
|
+
/** IAM roles and users in the collection, and how many of them no declared binding names; null when unknown. */
|
|
178
|
+
readonly observed: number | null;
|
|
179
|
+
readonly unclaimed: number | null;
|
|
180
|
+
}
|
|
181
|
+
/** An IAM role or user in a complete, fresh collection whose ARN no declared execution binding names. */
|
|
182
|
+
export interface MapUnclaimedIdentity {
|
|
183
|
+
readonly arn: string;
|
|
184
|
+
readonly kind: 'role' | 'user';
|
|
185
|
+
readonly name: string;
|
|
186
|
+
readonly path: string;
|
|
187
|
+
readonly tenantId: string;
|
|
188
|
+
readonly partition: string;
|
|
189
|
+
readonly accountId: string;
|
|
190
|
+
}
|
|
191
|
+
/** Only for `unclaimedIdentities` reads (all-teams readers). */
|
|
192
|
+
export interface MapUnclaimedIdentities {
|
|
193
|
+
readonly accounts: readonly MapCloudAccount[];
|
|
194
|
+
/** One page over the complete accounts, ordered by ARN. */
|
|
195
|
+
readonly identities: readonly MapUnclaimedIdentity[];
|
|
196
|
+
/** Unclaimed identities across the complete accounts. */
|
|
197
|
+
readonly total: number;
|
|
198
|
+
/** Some declared execution binding names no identity, so a listed identity may be its own. */
|
|
199
|
+
readonly bindingsWithoutIdentity: boolean;
|
|
71
200
|
}
|
|
72
201
|
export interface MapSource {
|
|
73
202
|
readonly id: string;
|
|
@@ -79,13 +208,64 @@ export interface MapSource {
|
|
|
79
208
|
}
|
|
80
209
|
export interface MapView {
|
|
81
210
|
readonly sources: readonly MapSource[];
|
|
211
|
+
/** `unclaimedIdentities` reads only. */
|
|
212
|
+
readonly unclaimedIdentities?: MapUnclaimedIdentities;
|
|
213
|
+
}
|
|
214
|
+
/** Narrows the policy evidence of a read to the pairs its caller will show. */
|
|
215
|
+
export interface MapPolicySelection {
|
|
216
|
+
/** Only pairs of this task (native or entity id). */
|
|
217
|
+
readonly taskId?: string;
|
|
218
|
+
/** Only pairs of this tool. */
|
|
219
|
+
readonly tool?: string;
|
|
220
|
+
/** Only pairs of bindings with this exact resource reference or mapped ARN. */
|
|
221
|
+
readonly resourceRef?: string;
|
|
222
|
+
/** Only this action. */
|
|
223
|
+
readonly action?: string;
|
|
224
|
+
/** Past the pair limit, answer with `policyEvidenceNotLoaded` instead of failing the read. */
|
|
225
|
+
readonly optional?: boolean;
|
|
82
226
|
}
|
|
83
227
|
export interface MapReadRequest {
|
|
84
228
|
readonly sourceId?: string;
|
|
85
229
|
readonly agentId?: string;
|
|
230
|
+
/** With agentId: also load boundary evidence (get_agent_access). */
|
|
231
|
+
readonly boundary?: boolean;
|
|
232
|
+
/** A flags read for a page of agents (1-100 native or entity ids), with one-agent rules. */
|
|
233
|
+
readonly agentIds?: readonly string[];
|
|
86
234
|
readonly history?: boolean;
|
|
87
235
|
/** Agent/deployment index only; avoids loading task neighborhoods before selecting an agent. */
|
|
88
236
|
readonly summary?: boolean;
|
|
237
|
+
/** Full graph for boundary flags, without policy or identity evidence reads. */
|
|
238
|
+
readonly flags?: boolean;
|
|
239
|
+
/** Full graph plus identity reach limited to the buckets visible bindings map to. */
|
|
240
|
+
readonly outsideGuard?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* Owner index only (list_owners): agents, resource bindings and owners, plus, for a team
|
|
243
|
+
* reader, the nodes that decide which bindings are visible. No policy evidence.
|
|
244
|
+
*/
|
|
245
|
+
readonly owners?: boolean;
|
|
246
|
+
/**
|
|
247
|
+
* Policy evidence of the current projection. Omitted: every visible task/binding/action pair,
|
|
248
|
+
* at most 512 per read (more fails the read). `false`: none, so the read is not bound by
|
|
249
|
+
* that limit. An object narrows the pairs. Summary, history, flags, owner and discovery
|
|
250
|
+
* reads never load policy evidence.
|
|
251
|
+
*/
|
|
252
|
+
readonly policy?: false | MapPolicySelection;
|
|
253
|
+
/**
|
|
254
|
+
* All-teams readers only. Per source, counts of tasks and caller principals that no declared
|
|
255
|
+
* agent accounts for; no nodes are loaded. With `sourceId` and `page`, also one page of them.
|
|
256
|
+
*/
|
|
257
|
+
readonly undeclared?: boolean;
|
|
258
|
+
/**
|
|
259
|
+
* All-teams readers only, never with `sourceId`. IAM roles and users of the enrolled AWS
|
|
260
|
+
* collections that no declared execution binding names, one `page` at a time.
|
|
261
|
+
*/
|
|
262
|
+
readonly unclaimedIdentities?: boolean;
|
|
263
|
+
/** `kind` is required with `undeclared` and refused with `unclaimedIdentities`. */
|
|
264
|
+
readonly page?: {
|
|
265
|
+
readonly kind?: 'tasks' | 'principals';
|
|
266
|
+
readonly offset: number;
|
|
267
|
+
readonly limit: number;
|
|
268
|
+
};
|
|
89
269
|
}
|
|
90
270
|
export interface MapStore {
|
|
91
271
|
read(scope: ReadScope, request?: MapReadRequest): Promise<MapView>;
|
package/dist/types.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { immutableJson } from '@shieldaai/agent-guard';
|
|
1
|
+
import { immutableJson } from '@shieldaai/agent-guard/internal';
|
|
2
2
|
export class MapQueryError extends Error {
|
|
3
3
|
code;
|
|
4
4
|
constructor(code) {
|
|
@@ -7,15 +7,52 @@ export class MapQueryError extends Error {
|
|
|
7
7
|
this.name = 'MapQueryError';
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
-
export const MAP_LABELS = [
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
10
|
+
export const MAP_LABELS = [
|
|
11
|
+
'GuardAgent',
|
|
12
|
+
'GuardDeployment',
|
|
13
|
+
'GuardTask',
|
|
14
|
+
'GuardPrincipal',
|
|
15
|
+
'GuardGrant',
|
|
16
|
+
'GuardTool',
|
|
17
|
+
'GuardResource',
|
|
18
|
+
'GuardResourceBinding',
|
|
19
|
+
'GuardExecutionBinding',
|
|
20
|
+
'GuardExternalResource',
|
|
21
|
+
'GuardExternalIdentity',
|
|
22
|
+
'GuardMcpConnection',
|
|
23
|
+
'GuardMcpOperation',
|
|
24
|
+
'GuardOwner',
|
|
25
|
+
'GuardCredentialReference',
|
|
26
|
+
'GuardDirectAccess',
|
|
27
|
+
];
|
|
28
|
+
export const MAP_RELATIONSHIPS = [
|
|
29
|
+
'G_INSTANCE_OF',
|
|
30
|
+
'G_ASSIGNED_TO',
|
|
31
|
+
'G_ACTS_AS',
|
|
32
|
+
'G_DELEGATED_FROM',
|
|
33
|
+
'G_DERIVED_FROM',
|
|
34
|
+
'G_HAS_GRANT',
|
|
35
|
+
'G_FOR_TOOL',
|
|
36
|
+
'G_TARGETS',
|
|
37
|
+
'G_BINDS_SELECTOR',
|
|
38
|
+
'G_MAPS_TO',
|
|
39
|
+
'G_EXECUTES_FOR',
|
|
40
|
+
'G_EXECUTES_TOOL',
|
|
41
|
+
'G_USES_IDENTITY',
|
|
42
|
+
'G_VIA_CONNECTION',
|
|
43
|
+
'G_EXPOSES_OPERATION',
|
|
44
|
+
'G_OPERATION_TOOL',
|
|
45
|
+
'G_OWNED_BY',
|
|
46
|
+
'G_RESOURCE_OWNED_BY',
|
|
47
|
+
'G_USES_CREDENTIAL',
|
|
48
|
+
'G_DEPENDS_ON',
|
|
49
|
+
'G_DIRECT_ACCESS_FOR',
|
|
50
|
+
];
|
|
17
51
|
export function mapIdentifier(value, code = 'MAP_REQUEST_INVALID') {
|
|
18
|
-
if (typeof value !== 'string' ||
|
|
52
|
+
if (typeof value !== 'string' ||
|
|
53
|
+
!value.trim() ||
|
|
54
|
+
[...value].length > 4096 ||
|
|
55
|
+
value.includes('\0') ||
|
|
19
56
|
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value))
|
|
20
57
|
throw new MapQueryError(code);
|
|
21
58
|
return value;
|
|
@@ -23,7 +60,9 @@ export function mapIdentifier(value, code = 'MAP_REQUEST_INVALID') {
|
|
|
23
60
|
export function validateReadScope(value) {
|
|
24
61
|
try {
|
|
25
62
|
const copied = immutableJson(value, 4, 1024, 64 * 1024);
|
|
26
|
-
if (!copied ||
|
|
63
|
+
if (!copied ||
|
|
64
|
+
Array.isArray(copied) ||
|
|
65
|
+
typeof copied !== 'object' ||
|
|
27
66
|
Object.keys(copied).sort().join(',') !== 'allTeams,companyId,subject,teamIds,tenantIds')
|
|
28
67
|
throw new Error();
|
|
29
68
|
mapIdentifier(copied.subject);
|
|
@@ -32,7 +71,9 @@ export function validateReadScope(value) {
|
|
|
32
71
|
throw new Error();
|
|
33
72
|
for (const key of ['tenantIds', 'teamIds']) {
|
|
34
73
|
const items = copied[key];
|
|
35
|
-
if (!Array.isArray(items) ||
|
|
74
|
+
if (!Array.isArray(items) ||
|
|
75
|
+
items.length > 128 ||
|
|
76
|
+
(key === 'tenantIds' && items.length === 0) ||
|
|
36
77
|
new Set(items).size !== items.length)
|
|
37
78
|
throw new Error();
|
|
38
79
|
items.forEach(item => mapIdentifier(item));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shieldaai/agent-map",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Read-only AI agent and MCP inventory queries over HTTP and MCP, with scoped access to Cartography projections.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -10,12 +10,25 @@
|
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./service": {
|
|
15
|
+
"types": "./dist/service.d.ts",
|
|
16
|
+
"import": "./dist/service.js"
|
|
17
|
+
},
|
|
18
|
+
"./oidc": {
|
|
19
|
+
"types": "./dist/oidc.d.ts",
|
|
20
|
+
"import": "./dist/oidc.js"
|
|
21
|
+
},
|
|
22
|
+
"./identity-http": {
|
|
23
|
+
"types": "./dist/identity-http.d.ts",
|
|
24
|
+
"import": "./dist/identity-http.js"
|
|
13
25
|
}
|
|
14
26
|
},
|
|
15
27
|
"bin": {
|
|
16
28
|
"agent-guard-map": "./dist/cli.js",
|
|
17
29
|
"agent-guard-map-identity": "./dist/identity-cli.js"
|
|
18
30
|
},
|
|
31
|
+
"sideEffects": false,
|
|
19
32
|
"files": [
|
|
20
33
|
"dist",
|
|
21
34
|
"README.md",
|
|
@@ -26,13 +39,13 @@
|
|
|
26
39
|
},
|
|
27
40
|
"dependencies": {
|
|
28
41
|
"@modelcontextprotocol/server": "2.4.0",
|
|
29
|
-
"@shieldaai/agent-guard": "
|
|
42
|
+
"@shieldaai/agent-guard": "1.0.0",
|
|
30
43
|
"neo4j-driver": "6.3.0",
|
|
31
44
|
"openid-client": "6.8.8"
|
|
32
45
|
},
|
|
33
46
|
"publishConfig": {
|
|
34
47
|
"access": "public",
|
|
35
|
-
"tag": "
|
|
48
|
+
"tag": "latest"
|
|
36
49
|
},
|
|
37
50
|
"repository": {
|
|
38
51
|
"type": "git",
|