@yiaany/ghostapi 0.1.9 → 0.2.1
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/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +132 -119
- package/LICENSE +21 -21
- package/PROVENANCE.md +13 -0
- package/README.md +78 -84
- package/SECURITY.md +56 -53
- package/dist/actions/gateway.js +320 -72
- package/dist/actions/gateway.js.map +1 -1
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +1 -1
- package/dist/actions/index.js.map +1 -1
- package/dist/agents/agentPrompt.js +7 -5
- package/dist/agents/agentPrompt.js.map +1 -1
- package/dist/ai/aiClient.js +5 -5
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.js +66 -26
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/genericInference.js +151 -36
- package/dist/ai/genericInference.js.map +1 -1
- package/dist/ai/prompts.js +55 -49
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.js +649 -133
- package/dist/approvals/approvalInbox.js.map +1 -1
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +1 -1
- package/dist/approvals/index.js.map +1 -1
- package/dist/behavior/behaviorStore.js +34 -14
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.js +14 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +226 -93
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.js +17 -3
- package/dist/cli/openUrl.js.map +1 -1
- package/dist/cli/parser.js +79 -18
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.js +4 -2
- package/dist/config/dataPaths.js.map +1 -1
- package/dist/config/localConfig.js +5 -2
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.js +11 -5
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.js +311 -83
- package/dist/contracts/contract.js.map +1 -1
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/credentials/credentialBroker.js +391 -79
- package/dist/credentials/credentialBroker.js.map +1 -1
- package/dist/credentials/index.d.ts +2 -2
- package/dist/credentials/index.js +1 -1
- package/dist/credentials/index.js.map +1 -1
- package/dist/dashboard/app.js +823 -685
- package/dist/dashboard/dashboard.js.map +1 -1
- package/dist/dashboard/index.html +704 -267
- package/dist/dashboard/styles.css +1040 -349
- package/dist/egress/capabilities.js +55 -25
- package/dist/egress/capabilities.js.map +1 -1
- package/dist/egress/linuxBootstrap.js +47 -11
- package/dist/egress/linuxBootstrap.js.map +1 -1
- package/dist/egress/run.js +170 -31
- package/dist/egress/run.js.map +1 -1
- package/dist/errors/errorEngine.js +15 -3
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/errors/providerErrors.js.map +1 -1
- package/dist/evals/evals.js +304 -78
- package/dist/evals/evals.js.map +1 -1
- package/dist/evals/index.d.ts +2 -2
- package/dist/evals/index.js +1 -1
- package/dist/evals/index.js.map +1 -1
- package/dist/evidence/index.d.ts +2 -2
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/index.js.map +1 -1
- package/dist/evidence/report.d.ts +1 -1
- package/dist/evidence/report.js +263 -79
- package/dist/evidence/report.js.map +1 -1
- package/dist/fault/faultLab.js +24 -9
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +42 -42
- package/dist/index.js +21 -21
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +1 -1
- package/dist/inventory/analysis.js +174 -50
- package/dist/inventory/analysis.js.map +1 -1
- package/dist/inventory/index.d.ts +4 -4
- package/dist/inventory/index.js +3 -3
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/inventoryController.js +362 -115
- package/dist/inventory/inventoryController.js.map +1 -1
- package/dist/inventory/types.js +680 -140
- package/dist/inventory/types.js.map +1 -1
- package/dist/ledger/actionLedger.js +355 -78
- package/dist/ledger/actionLedger.js.map +1 -1
- package/dist/ledger/index.d.ts +2 -2
- package/dist/ledger/index.js +1 -1
- package/dist/ledger/index.js.map +1 -1
- package/dist/mcp/server.js +26 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +1 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/policy.js +112 -32
- package/dist/policy/policy.js.map +1 -1
- package/dist/productTelemetry/index.js +71 -20
- package/dist/productTelemetry/index.js.map +1 -1
- package/dist/providers/conformance.js +10 -5
- package/dist/providers/conformance.js.map +1 -1
- package/dist/providers/discord.js +2 -2
- package/dist/providers/generic.js +3 -3
- package/dist/providers/github.js +2 -2
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/packs/resendPack.js +51 -25
- package/dist/providers/packs/resendPack.js.map +1 -1
- package/dist/providers/packs/stripePack.js +590 -121
- package/dist/providers/packs/stripePack.js.map +1 -1
- package/dist/providers/registry.js +4 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/runtime.js +7 -6
- package/dist/providers/runtime.js.map +1 -1
- package/dist/providers/stripeWebhook.js +39 -10
- package/dist/providers/stripeWebhook.js.map +1 -1
- package/dist/providers/twilio.js +2 -2
- package/dist/proxy/cacheKey.js +9 -2
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.js +22 -9
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +57 -22
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.js +4 -2
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.js +211 -49
- package/dist/reliability/costGovernance.js.map +1 -1
- package/dist/reliability/index.d.ts +8 -8
- package/dist/reliability/index.js +4 -4
- package/dist/reliability/index.js.map +1 -1
- package/dist/reliability/reconciliation.js +274 -65
- package/dist/reliability/reconciliation.js.map +1 -1
- package/dist/reliability/runtimeHealth.js +127 -35
- package/dist/reliability/runtimeHealth.js.map +1 -1
- package/dist/reliability/slo.js +257 -50
- package/dist/reliability/slo.js.map +1 -1
- package/dist/report/safetyReport.d.ts +6 -0
- package/dist/report/safetyReport.js +56 -12
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/rules/aiRules.js +5 -5
- package/dist/rules/aiRules.js.map +1 -1
- package/dist/safety/index.d.ts +2 -2
- package/dist/safety/index.js +1 -1
- package/dist/safety/index.js.map +1 -1
- package/dist/safety/safetyController.js +675 -165
- package/dist/safety/safetyController.js.map +1 -1
- package/dist/scenarios/scenarioBundle.js +204 -71
- package/dist/scenarios/scenarioBundle.js.map +1 -1
- package/dist/scenarios/scenarioStore.js +88 -29
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.js +52 -10
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +10 -5
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.js +37 -10
- package/dist/server/accessControl.js.map +1 -1
- package/dist/server/createServer.js +1 -1
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.js +15 -5
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +61 -19
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.js +1 -1
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/sdkDetector.js +7 -2
- package/dist/setup/sdkDetector.js.map +1 -1
- package/dist/setup/setupGenerator.js +136 -48
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateExtractor.js +3 -1
- package/dist/state/stateExtractor.js.map +1 -1
- package/dist/state/stateResolver.js +10 -7
- package/dist/state/stateResolver.js.map +1 -1
- package/dist/state/stateStore.js +13 -4
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.js +12 -4
- package/dist/storage/fileStore.js.map +1 -1
- package/dist/teamControl/controlPlane.js +846 -174
- package/dist/teamControl/controlPlane.js.map +1 -1
- package/dist/teamControl/deployment.js +19 -5
- package/dist/teamControl/deployment.js.map +1 -1
- package/dist/teamControl/index.d.ts +4 -4
- package/dist/teamControl/index.js +2 -2
- package/dist/teamControl/index.js.map +1 -1
- package/dist/tests/testGenerator.js +15 -7
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -2
- package/dist/trust/index.js +1 -1
- package/dist/trust/index.js.map +1 -1
- package/dist/trust/trustLadder.js +309 -63
- package/dist/trust/trustLadder.js.map +1 -1
- package/dist/worlds/index.d.ts +2 -2
- package/dist/worlds/index.js +1 -1
- package/dist/worlds/index.js.map +1 -1
- package/dist/worlds/worlds.js +364 -65
- package/dist/worlds/worlds.js.map +1 -1
- package/docs/assets/dashboard.png +0 -0
- package/docs/ci.md +49 -49
- package/docs/github-actions.md +84 -84
- package/docs/mcp.md +58 -52
- package/docs/policy.md +63 -61
- package/docs/providers/authoring-packs.md +180 -175
- package/docs/providers/stripe-core-pack.md +84 -82
- package/docs/releases/README.md +13 -0
- package/docs/security/action-gateway-threat-model.md +34 -34
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
- package/docs/security/approval-inbox-threat-model.md +35 -35
- package/docs/security/credential-broker-threat-model.md +52 -52
- package/docs/security/egress-threat-model.md +126 -126
- package/docs/security/inventory-threat-model.md +104 -104
- package/docs/security/kill-switch-budgets-threat-model.md +37 -37
- package/docs/security/reliability-threat-model.md +111 -111
- package/docs/security/trust-ladder-threat-model.md +33 -33
- package/docs/telemetry.md +14 -0
- package/docs/usage.md +680 -571
- package/examples/README.md +10 -10
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -21
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
- package/examples/ci-smoke/package.json +9 -9
- package/examples/ci-smoke/production-egress.mjs +4 -2
- package/examples/ci-smoke/safe.mjs +21 -17
- package/examples/evals/README.md +24 -24
- package/examples/evals/retry-after.eval.json +71 -52
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +38 -35
- package/examples/openai-streaming/README.md +16 -16
- package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
- package/examples/policy/ghostapi.policy.yaml +27 -27
- package/examples/record-replay/README.md +19 -19
- package/examples/record-replay/replay-requests.json +22 -22
- package/examples/record-replay/stripe-checkout.har.json +30 -30
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -35
- package/examples/stripe-node/checkout-flow.mjs +87 -61
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -19
- package/examples/worlds/subscription-recovery.mjs +21 -12
- package/package.json +28 -6
- package/dist/landing/assets/index-BXR41fAD.css +0 -1
- package/dist/landing/assets/index-CxWsopzP.css +0 -1
- package/dist/landing/assets/index-D8_ggMRE.js +0 -55
- package/dist/landing/assets/index-DZlpyhqd.js +0 -55
- package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
- package/dist/landing/index.html +0 -15
- package/dist/landing/landing.d.ts +0 -3
- package/dist/landing/landing.js +0 -12
- package/dist/landing/landing.js.map +0 -1
- package/docs/commercial/README.md +0 -29
- package/docs/commercial/data-inventory.md +0 -36
- package/docs/commercial/manual-invoicing.md +0 -44
- package/docs/commercial/metrics.md +0 -51
- package/docs/commercial/pricing.md +0 -103
- package/docs/commercial/privacy-policy-draft.md +0 -41
- package/docs/commercial/terms-topics-for-counsel.md +0 -51
- package/docs/design-partners/README.md +0 -52
- package/docs/design-partners/demo-narratives.md +0 -33
- package/docs/design-partners/design-partner-offer.md +0 -39
- package/docs/design-partners/discovery-questions.md +0 -28
- package/docs/design-partners/feedback-capture-template.md +0 -48
- package/docs/design-partners/icp-one-pager.md +0 -43
- package/docs/design-partners/onboarding-checklist.md +0 -33
- package/docs/design-partners/pilot-success-criteria.md +0 -27
- package/docs/design-partners/pricing-interview-script.md +0 -18
- package/docs/design-partners/telemetry-plan.md +0 -48
- package/docs/development/baseline.md +0 -114
- package/docs/development/onboarding-smoke.md +0 -37
- package/docs/development/verification-0.1.8.md +0 -25
- package/docs/enterprise-product-roadmap-ru.md +0 -1879
- package/docs/fundraising/README.md +0 -87
- package/docs/fundraising/data-room-checklist.md +0 -72
- package/docs/fundraising/demo-script.md +0 -52
- package/docs/fundraising/design-partners-50.md +0 -74
- package/docs/fundraising/launch-posts.md +0 -37
- package/docs/fundraising/metrics-and-evidence.md +0 -30
- package/docs/fundraising/roadmap-12-month.md +0 -25
- package/docs/fundraising/technical-due-diligence-index.md +0 -37
- package/docs/fundraising/yc-application.md +0 -25
- package/docs/hosted-pilot.md +0 -111
- package/docs/operations/disaster-recovery-runbook.md +0 -81
- package/docs/operations/kill-switch-runbook.md +0 -32
- package/docs/release-checklist.md +0 -83
- package/docs/release-migration-and-rollback.md +0 -63
- package/docs/release-readiness.md +0 -39
- package/docs/team-control-plane.md +0 -72
|
@@ -2,11 +2,11 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { lstat, readFile } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { getDataPaths } from "../config/dataPaths.js";
|
|
5
|
-
import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
|
|
5
|
+
import { atomicWriteJson, ensurePrivateDirectory, withFileLock, } from "../storage/fileStore.js";
|
|
6
6
|
import { sanitizeSecretString } from "../security/secrets.js";
|
|
7
7
|
import { normalizeFreshnessDays } from "./analysis.js";
|
|
8
|
-
import { InventoryError, INVENTORY_LIMITS, buildRecordFreshness, buildRecordProvenance, canonicalPayloadDigest, clone, edgeIdFor, emptyInventoryState, freshnessStatusFor, validateImportPayload, validateOperator, validateState } from "./types.js";
|
|
9
|
-
import { computeBlastRadius, computeFindings, computeRemovalAnalysis, computeRoiReport, findAttackPaths, findingKey, graphEdges } from "./analysis.js";
|
|
8
|
+
import { InventoryError, INVENTORY_LIMITS, buildRecordFreshness, buildRecordProvenance, canonicalPayloadDigest, clone, edgeIdFor, emptyInventoryState, freshnessStatusFor, validateImportPayload, validateOperator, validateState, } from "./types.js";
|
|
9
|
+
import { computeBlastRadius, computeFindings, computeRemovalAnalysis, computeRoiReport, findAttackPaths, findingKey, graphEdges, } from "./analysis.js";
|
|
10
10
|
export class LocalInventoryController {
|
|
11
11
|
path;
|
|
12
12
|
now;
|
|
@@ -16,7 +16,8 @@ export class LocalInventoryController {
|
|
|
16
16
|
constructor(options = {}) {
|
|
17
17
|
this.path = options.path ?? getDataPaths().inventoryStore;
|
|
18
18
|
this.now = options.now ?? (() => new Date());
|
|
19
|
-
this.operatorAuthorizer =
|
|
19
|
+
this.operatorAuthorizer =
|
|
20
|
+
options.operatorAuthorizer ?? createDisabledInventoryOperatorAuthorizer();
|
|
20
21
|
this.freshnessDays = options.freshnessDays;
|
|
21
22
|
this.evalScenarioExists = options.evalScenarioExists;
|
|
22
23
|
}
|
|
@@ -32,7 +33,16 @@ export class LocalInventoryController {
|
|
|
32
33
|
for (const edge of state.edges)
|
|
33
34
|
before.set(edge.edgeId, edge);
|
|
34
35
|
const upserted = [];
|
|
35
|
-
const counts = {
|
|
36
|
+
const counts = {
|
|
37
|
+
agents: 0,
|
|
38
|
+
tools: 0,
|
|
39
|
+
identities: 0,
|
|
40
|
+
providers: 0,
|
|
41
|
+
resources: 0,
|
|
42
|
+
sideEffects: 0,
|
|
43
|
+
credentials: 0,
|
|
44
|
+
policies: 0,
|
|
45
|
+
};
|
|
36
46
|
const imported = new ImportedRecords(payload);
|
|
37
47
|
for (const input of payload.agents ?? []) {
|
|
38
48
|
upsertAgent(state, operator.tenantId, input, payload, operator.principalId, now);
|
|
@@ -85,17 +95,33 @@ export class LocalInventoryController {
|
|
|
85
95
|
runId,
|
|
86
96
|
importedAt: now,
|
|
87
97
|
importedBy: operator.principalId,
|
|
88
|
-
source: {
|
|
98
|
+
source: {
|
|
99
|
+
sourceId: payload.source.sourceId,
|
|
100
|
+
sourceType: payload.source.sourceType,
|
|
101
|
+
sourceName: payload.source.sourceName,
|
|
102
|
+
...(payload.source.sourceVersion === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: { sourceVersion: payload.source.sourceVersion }),
|
|
105
|
+
},
|
|
89
106
|
digest,
|
|
90
107
|
expectedPolicyHashes: payload.expectedPolicyHashes ?? [],
|
|
91
|
-
...(payload.counters === undefined
|
|
108
|
+
...(payload.counters === undefined
|
|
109
|
+
? {}
|
|
110
|
+
: { counters: payload.counters }),
|
|
92
111
|
recordCounts: counts,
|
|
93
112
|
edgesCreated: outcome.edgesCreated,
|
|
94
113
|
edgesRefreshed: outcome.edgesRefreshed,
|
|
95
|
-
status: "completed"
|
|
114
|
+
status: "completed",
|
|
96
115
|
});
|
|
97
116
|
this.rotateImportRuns(state, operator.tenantId);
|
|
98
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
runId,
|
|
119
|
+
importedAt: now,
|
|
120
|
+
digest,
|
|
121
|
+
recordCounts: counts,
|
|
122
|
+
edgesCreated: outcome.edgesCreated,
|
|
123
|
+
edgesRefreshed: outcome.edgesRefreshed,
|
|
124
|
+
};
|
|
99
125
|
});
|
|
100
126
|
}
|
|
101
127
|
async inspect(identityValue) {
|
|
@@ -114,23 +140,41 @@ export class LocalInventoryController {
|
|
|
114
140
|
}
|
|
115
141
|
const computed = computeFindings(state, operator.tenantId, now, this.freshnessDays);
|
|
116
142
|
let resolved = 0;
|
|
117
|
-
const appliedRemediations = state.remediations.filter((remediation) => remediation.tenantId === operator.tenantId &&
|
|
143
|
+
const appliedRemediations = state.remediations.filter((remediation) => remediation.tenantId === operator.tenantId &&
|
|
144
|
+
remediation.status === "applied");
|
|
118
145
|
const nextFindings = computed.map((finding) => {
|
|
119
146
|
const prior = previous.get(findingKey(finding));
|
|
120
|
-
const matchingRemediation = appliedRemediations.find((remediation) => remediation.findingId === finding.findingId &&
|
|
147
|
+
const matchingRemediation = appliedRemediations.find((remediation) => remediation.findingId === finding.findingId &&
|
|
148
|
+
remediation.targetKind === finding.targetKind &&
|
|
149
|
+
remediation.targetId === finding.targetId);
|
|
121
150
|
if (matchingRemediation !== undefined) {
|
|
122
151
|
if (this.isRemediationEffective(state, operator.tenantId, matchingRemediation)) {
|
|
123
152
|
resolved += 1;
|
|
124
|
-
return {
|
|
153
|
+
return {
|
|
154
|
+
...finding,
|
|
155
|
+
status: "resolved",
|
|
156
|
+
resolvedAt: matchingRemediation.appliedAt,
|
|
157
|
+
remediationId: matchingRemediation.remediationId,
|
|
158
|
+
};
|
|
125
159
|
}
|
|
126
160
|
return finding;
|
|
127
161
|
}
|
|
128
162
|
if (prior !== undefined && prior.status === "resolved") {
|
|
129
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
...finding,
|
|
165
|
+
status: "resolved",
|
|
166
|
+
resolvedAt: prior.resolvedAt,
|
|
167
|
+
...(prior.remediationId === undefined
|
|
168
|
+
? {}
|
|
169
|
+
: { remediationId: prior.remediationId }),
|
|
170
|
+
};
|
|
130
171
|
}
|
|
131
172
|
return finding;
|
|
132
173
|
});
|
|
133
|
-
state.findings = [
|
|
174
|
+
state.findings = [
|
|
175
|
+
...state.findings.filter((finding) => finding.tenantId !== operator.tenantId),
|
|
176
|
+
...nextFindings,
|
|
177
|
+
];
|
|
134
178
|
this.assertStoreBounds(state);
|
|
135
179
|
return { findings: nextFindings.length, resolved };
|
|
136
180
|
});
|
|
@@ -165,18 +209,21 @@ export class LocalInventoryController {
|
|
|
165
209
|
const proposal = validateRemediationProposal(input);
|
|
166
210
|
const now = this.timestamp();
|
|
167
211
|
return this.mutate((state) => {
|
|
168
|
-
const finding = state.findings.find((candidate) => candidate.tenantId === operator.tenantId &&
|
|
212
|
+
const finding = state.findings.find((candidate) => candidate.tenantId === operator.tenantId &&
|
|
213
|
+
candidate.findingId === proposal.findingId);
|
|
169
214
|
if (finding === undefined)
|
|
170
215
|
throw new InventoryError("Remediation finding was not found for the tenant.");
|
|
171
216
|
if (finding.status === "resolved")
|
|
172
217
|
throw new InventoryError("Cannot propose remediation for a resolved finding.");
|
|
173
|
-
if (proposal.targetKind !== finding.targetKind ||
|
|
218
|
+
if (proposal.targetKind !== finding.targetKind ||
|
|
219
|
+
proposal.targetId !== finding.targetId)
|
|
174
220
|
throw new InventoryError("Remediation target does not match the finding target.");
|
|
175
221
|
this.assertRemediationTargetExists(state, operator.tenantId, proposal.targetKind, proposal.targetId);
|
|
176
222
|
if (proposal.kind === "reduce_scope") {
|
|
177
223
|
if (proposal.targetKind !== "credential")
|
|
178
224
|
throw new InventoryError("reduce_scope remediation only supports credential targets.");
|
|
179
|
-
const credential = state.credentials.find((candidate) => candidate.tenantId === operator.tenantId &&
|
|
225
|
+
const credential = state.credentials.find((candidate) => candidate.tenantId === operator.tenantId &&
|
|
226
|
+
candidate.credentialId === proposal.targetId);
|
|
180
227
|
if (credential === undefined)
|
|
181
228
|
throw new InventoryError("Remediation target credential does not exist.");
|
|
182
229
|
if (credential.status !== "active")
|
|
@@ -188,7 +235,9 @@ export class LocalInventoryController {
|
|
|
188
235
|
if (reduced.length >= credential.grantScopes.length)
|
|
189
236
|
throw new InventoryError("reduce_scope must remove at least one scope; permissions are never expanded.");
|
|
190
237
|
}
|
|
191
|
-
if (proposal.kind === "create_eval" &&
|
|
238
|
+
if (proposal.kind === "create_eval" &&
|
|
239
|
+
this.evalScenarioExists !== undefined &&
|
|
240
|
+
!this.evalScenarioExists(proposal.evalScenarioId))
|
|
192
241
|
throw new InventoryError("Eval scenario was not found for create_eval remediation.");
|
|
193
242
|
if (state.remediations.length >= INVENTORY_LIMITS.maxRemediations)
|
|
194
243
|
throw new InventoryError("Remediation limit was reached.");
|
|
@@ -205,7 +254,11 @@ export class LocalInventoryController {
|
|
|
205
254
|
proposedBy: operator.principalId,
|
|
206
255
|
createdAt: now,
|
|
207
256
|
status: "proposed",
|
|
208
|
-
...(proposal.ownerId === undefined &&
|
|
257
|
+
...(proposal.ownerId === undefined &&
|
|
258
|
+
proposal.reducedScopes === undefined &&
|
|
259
|
+
proposal.evalScenarioId === undefined
|
|
260
|
+
? {}
|
|
261
|
+
: { result: remediationResult(proposal, operator.principalId) }),
|
|
209
262
|
};
|
|
210
263
|
state.remediations.push(record);
|
|
211
264
|
this.assertStoreBounds(state);
|
|
@@ -217,7 +270,8 @@ export class LocalInventoryController {
|
|
|
217
270
|
const now = this.timestamp();
|
|
218
271
|
const safeRemediationId = safeIdentifier(remediationId, "Remediation id");
|
|
219
272
|
return this.mutate((state) => {
|
|
220
|
-
const index = state.remediations.findIndex((candidate) => candidate.tenantId === operator.tenantId &&
|
|
273
|
+
const index = state.remediations.findIndex((candidate) => candidate.tenantId === operator.tenantId &&
|
|
274
|
+
candidate.remediationId === safeRemediationId);
|
|
221
275
|
if (index === -1)
|
|
222
276
|
throw new InventoryError("Remediation was not found for the tenant.");
|
|
223
277
|
const remediation = state.remediations[index];
|
|
@@ -226,9 +280,14 @@ export class LocalInventoryController {
|
|
|
226
280
|
if (remediation.status === "rejected" || remediation.status === "expired")
|
|
227
281
|
throw new InventoryError(`Remediation is ${remediation.status} and cannot be applied.`);
|
|
228
282
|
applyRemediationEffect(state, operator.tenantId, remediation, now);
|
|
229
|
-
const applied = {
|
|
283
|
+
const applied = {
|
|
284
|
+
...remediation,
|
|
285
|
+
status: "applied",
|
|
286
|
+
appliedAt: now,
|
|
287
|
+
};
|
|
230
288
|
state.remediations[index] = applied;
|
|
231
|
-
const finding = state.findings.find((candidate) => candidate.tenantId === operator.tenantId &&
|
|
289
|
+
const finding = state.findings.find((candidate) => candidate.tenantId === operator.tenantId &&
|
|
290
|
+
candidate.findingId === remediation.findingId);
|
|
232
291
|
if (finding !== undefined) {
|
|
233
292
|
finding.status = "resolved";
|
|
234
293
|
finding.resolvedAt = now;
|
|
@@ -240,10 +299,10 @@ export class LocalInventoryController {
|
|
|
240
299
|
}
|
|
241
300
|
async rejectRemediation(identityValue, remediationId) {
|
|
242
301
|
const operator = await this.authorize(identityValue, "inventory.remediate");
|
|
243
|
-
const now = this.timestamp();
|
|
244
302
|
const safeRemediationId = safeIdentifier(remediationId, "Remediation id");
|
|
245
303
|
return this.mutate((state) => {
|
|
246
|
-
const index = state.remediations.findIndex((candidate) => candidate.tenantId === operator.tenantId &&
|
|
304
|
+
const index = state.remediations.findIndex((candidate) => candidate.tenantId === operator.tenantId &&
|
|
305
|
+
candidate.remediationId === safeRemediationId);
|
|
247
306
|
if (index === -1)
|
|
248
307
|
throw new InventoryError("Remediation was not found for the tenant.");
|
|
249
308
|
const remediation = state.remediations[index];
|
|
@@ -266,13 +325,22 @@ export class LocalInventoryController {
|
|
|
266
325
|
const tenantId = operator.tenantId;
|
|
267
326
|
const snapshot = snapshotFor(state, tenantId, now, this.freshnessDays);
|
|
268
327
|
const scenarioRefs = state.remediations
|
|
269
|
-
.filter((remediation) => remediation.tenantId === tenantId &&
|
|
270
|
-
|
|
328
|
+
.filter((remediation) => remediation.tenantId === tenantId &&
|
|
329
|
+
remediation.status === "applied" &&
|
|
330
|
+
remediation.kind === "create_eval" &&
|
|
331
|
+
remediation.result?.evalScenarioId !== undefined)
|
|
332
|
+
.map((remediation) => ({
|
|
333
|
+
evalScenarioId: remediation.result.evalScenarioId,
|
|
334
|
+
forFindingId: remediation.findingId,
|
|
335
|
+
createdBy: remediation.proposedBy,
|
|
336
|
+
}));
|
|
271
337
|
const normalized = normalizeFreshnessDays(this.freshnessDays);
|
|
272
338
|
const evidenceMetadata = snapshot.agents.map((agent) => ({
|
|
273
339
|
agentId: agent.agentId,
|
|
274
340
|
lastEvidenceAt: agent.lastEvidenceAt,
|
|
275
|
-
status: agent.lastEvidenceAt === null
|
|
341
|
+
status: agent.lastEvidenceAt === null
|
|
342
|
+
? "missing"
|
|
343
|
+
: freshnessStatusFor(agent.lastEvidenceAt, now, normalized.evidenceFreshDays),
|
|
276
344
|
}));
|
|
277
345
|
return {
|
|
278
346
|
schemaVersion: 1,
|
|
@@ -284,7 +352,7 @@ export class LocalInventoryController {
|
|
|
284
352
|
scenarioRefs,
|
|
285
353
|
evidenceMetadata,
|
|
286
354
|
removalAnalysis: computeRemovalAnalysis(state, tenantId, now, this.freshnessDays),
|
|
287
|
-
roi: computeRoiReport(state, tenantId, now, this.freshnessDays)
|
|
355
|
+
roi: computeRoiReport(state, tenantId, now, this.freshnessDays),
|
|
288
356
|
};
|
|
289
357
|
}
|
|
290
358
|
async authorize(identity, permission) {
|
|
@@ -300,7 +368,8 @@ export class LocalInventoryController {
|
|
|
300
368
|
return value.toISOString();
|
|
301
369
|
}
|
|
302
370
|
upsertSource(state, tenantId, payload, now) {
|
|
303
|
-
const existing = state.sources.find((source) => source.tenantId === tenantId &&
|
|
371
|
+
const existing = state.sources.find((source) => source.tenantId === tenantId &&
|
|
372
|
+
source.sourceId === payload.source.sourceId);
|
|
304
373
|
if (existing === undefined) {
|
|
305
374
|
if (state.sources.length >= INVENTORY_LIMITS.maxSources)
|
|
306
375
|
throw new InventoryError("Import source limit was reached.");
|
|
@@ -309,10 +378,12 @@ export class LocalInventoryController {
|
|
|
309
378
|
sourceId: payload.source.sourceId,
|
|
310
379
|
sourceType: payload.source.sourceType,
|
|
311
380
|
sourceName: payload.source.sourceName,
|
|
312
|
-
...(payload.source.sourceVersion === undefined
|
|
381
|
+
...(payload.source.sourceVersion === undefined
|
|
382
|
+
? {}
|
|
383
|
+
: { sourceVersion: payload.source.sourceVersion }),
|
|
313
384
|
firstSeenAt: now,
|
|
314
385
|
lastSeenAt: now,
|
|
315
|
-
lastDigest: canonicalPayloadDigest(payload)
|
|
386
|
+
lastDigest: canonicalPayloadDigest(payload),
|
|
316
387
|
});
|
|
317
388
|
}
|
|
318
389
|
else {
|
|
@@ -323,20 +394,40 @@ export class LocalInventoryController {
|
|
|
323
394
|
}
|
|
324
395
|
}
|
|
325
396
|
assertRemediationTargetExists(state, tenantId, targetKind, targetId) {
|
|
326
|
-
const exists = targetKind === "agent"
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
397
|
+
const exists = targetKind === "agent"
|
|
398
|
+
? state.agents.some((record) => record.tenantId === tenantId && record.agentId === targetId)
|
|
399
|
+
: targetKind === "identity"
|
|
400
|
+
? state.identities.some((record) => record.tenantId === tenantId && record.identityId === targetId)
|
|
401
|
+
: targetKind === "tool"
|
|
402
|
+
? state.tools.some((record) => record.tenantId === tenantId && record.toolId === targetId)
|
|
403
|
+
: targetKind === "provider"
|
|
404
|
+
? state.providers.some((record) => record.tenantId === tenantId &&
|
|
405
|
+
record.providerId === targetId)
|
|
406
|
+
: targetKind === "resource"
|
|
407
|
+
? state.resources.some((record) => record.tenantId === tenantId &&
|
|
408
|
+
record.resourceId === targetId)
|
|
409
|
+
: targetKind === "side_effect"
|
|
410
|
+
? state.sideEffects.some((record) => record.tenantId === tenantId &&
|
|
411
|
+
record.sideEffectId === targetId)
|
|
412
|
+
: targetKind === "credential"
|
|
413
|
+
? state.credentials.some((record) => record.tenantId === tenantId &&
|
|
414
|
+
record.credentialId === targetId)
|
|
415
|
+
: targetKind === "environment"
|
|
416
|
+
? state.agents.some((record) => record.tenantId === tenantId &&
|
|
417
|
+
record.environmentIds.includes(targetId)) ||
|
|
418
|
+
state.identities.some((record) => record.tenantId === tenantId &&
|
|
419
|
+
record.environmentIds.includes(targetId)) ||
|
|
420
|
+
state.providers.some((record) => record.tenantId === tenantId &&
|
|
421
|
+
record.environmentIds.includes(targetId)) ||
|
|
422
|
+
state.resources.some((record) => record.tenantId === tenantId &&
|
|
423
|
+
record.environmentIds.includes(targetId)) ||
|
|
424
|
+
state.credentials.some((record) => record.tenantId === tenantId &&
|
|
425
|
+
record.environmentIds.includes(targetId)) ||
|
|
426
|
+
state.policies.some((record) => record.tenantId === tenantId &&
|
|
427
|
+
record.environmentIds.includes(targetId))
|
|
428
|
+
: targetKind === "policy"
|
|
429
|
+
? state.policies.some((record) => record.tenantId === tenantId &&
|
|
430
|
+
record.policyId === targetId)
|
|
340
431
|
: false;
|
|
341
432
|
if (!exists)
|
|
342
433
|
throw new InventoryError(`Remediation target ${targetKind} ${targetId} does not exist for the tenant.`);
|
|
@@ -350,21 +441,26 @@ export class LocalInventoryController {
|
|
|
350
441
|
pruneStaleEdges(state, tenantId, now) {
|
|
351
442
|
const normalized = normalizeFreshnessDays(this.freshnessDays);
|
|
352
443
|
const staleBeforeMs = Date.parse(now) - normalized.edgeStaleDays * 24 * 60 * 60 * 1000;
|
|
353
|
-
state.edges = state.edges.filter((edge) => edge.tenantId !== tenantId ||
|
|
444
|
+
state.edges = state.edges.filter((edge) => edge.tenantId !== tenantId ||
|
|
445
|
+
Date.parse(edge.lastSeenAt) >= staleBeforeMs);
|
|
354
446
|
}
|
|
355
447
|
rotateImportRuns(state, tenantId) {
|
|
356
448
|
const tenantRuns = state.importRuns.filter((run) => run.tenantId === tenantId);
|
|
357
449
|
if (tenantRuns.length <= INVENTORY_LIMITS.maxImports)
|
|
358
450
|
return;
|
|
359
451
|
const excess = tenantRuns.length - INVENTORY_LIMITS.maxImports;
|
|
360
|
-
const oldestRunIds = new Set([...tenantRuns]
|
|
452
|
+
const oldestRunIds = new Set([...tenantRuns]
|
|
453
|
+
.sort((a, b) => Date.parse(a.importedAt) - Date.parse(b.importedAt))
|
|
454
|
+
.slice(0, excess)
|
|
455
|
+
.map((run) => run.runId));
|
|
361
456
|
state.importRuns = state.importRuns.filter((run) => !oldestRunIds.has(run.runId));
|
|
362
457
|
}
|
|
363
458
|
isRemediationEffective(state, tenantId, remediation) {
|
|
364
459
|
if (remediation.kind === "reduce_scope") {
|
|
365
460
|
if (remediation.targetKind !== "credential")
|
|
366
461
|
return false;
|
|
367
|
-
const credential = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
462
|
+
const credential = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
463
|
+
candidate.credentialId === remediation.targetId);
|
|
368
464
|
const reduced = remediation.result?.reducedScopes;
|
|
369
465
|
if (credential === undefined || reduced === undefined)
|
|
370
466
|
return false;
|
|
@@ -373,7 +469,8 @@ export class LocalInventoryController {
|
|
|
373
469
|
if (remediation.kind === "revoke") {
|
|
374
470
|
if (remediation.targetKind !== "credential")
|
|
375
471
|
return false;
|
|
376
|
-
const credential = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
472
|
+
const credential = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
473
|
+
candidate.credentialId === remediation.targetId);
|
|
377
474
|
return credential !== undefined && credential.status === "revoked";
|
|
378
475
|
}
|
|
379
476
|
if (remediation.kind === "assign_owner") {
|
|
@@ -381,19 +478,23 @@ export class LocalInventoryController {
|
|
|
381
478
|
if (ownerId === undefined)
|
|
382
479
|
return false;
|
|
383
480
|
if (remediation.targetKind === "agent") {
|
|
384
|
-
const record = state.agents.find((candidate) => candidate.tenantId === tenantId &&
|
|
481
|
+
const record = state.agents.find((candidate) => candidate.tenantId === tenantId &&
|
|
482
|
+
candidate.agentId === remediation.targetId);
|
|
385
483
|
return record !== undefined && record.ownerId === ownerId;
|
|
386
484
|
}
|
|
387
485
|
if (remediation.targetKind === "provider") {
|
|
388
|
-
const record = state.providers.find((candidate) => candidate.tenantId === tenantId &&
|
|
486
|
+
const record = state.providers.find((candidate) => candidate.tenantId === tenantId &&
|
|
487
|
+
candidate.providerId === remediation.targetId);
|
|
389
488
|
return record !== undefined && record.ownerId === ownerId;
|
|
390
489
|
}
|
|
391
490
|
if (remediation.targetKind === "resource") {
|
|
392
|
-
const record = state.resources.find((candidate) => candidate.tenantId === tenantId &&
|
|
491
|
+
const record = state.resources.find((candidate) => candidate.tenantId === tenantId &&
|
|
492
|
+
candidate.resourceId === remediation.targetId);
|
|
393
493
|
return record !== undefined && record.ownerId === ownerId;
|
|
394
494
|
}
|
|
395
495
|
if (remediation.targetKind === "credential") {
|
|
396
|
-
const record = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
496
|
+
const record = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
497
|
+
candidate.credentialId === remediation.targetId);
|
|
397
498
|
return record !== undefined && record.ownerId === ownerId;
|
|
398
499
|
}
|
|
399
500
|
return false;
|
|
@@ -401,14 +502,17 @@ export class LocalInventoryController {
|
|
|
401
502
|
if (remediation.kind === "onboard_through_gateway") {
|
|
402
503
|
if (remediation.targetKind !== "agent")
|
|
403
504
|
return false;
|
|
404
|
-
const record = state.agents.find((candidate) => candidate.tenantId === tenantId &&
|
|
505
|
+
const record = state.agents.find((candidate) => candidate.tenantId === tenantId &&
|
|
506
|
+
candidate.agentId === remediation.targetId);
|
|
405
507
|
return record !== undefined && record.gatewayManaged === true;
|
|
406
508
|
}
|
|
407
509
|
if (remediation.kind === "create_eval") {
|
|
408
510
|
const scenarioId = remediation.result?.evalScenarioId;
|
|
409
511
|
if (scenarioId === undefined)
|
|
410
512
|
return false;
|
|
411
|
-
return this.evalScenarioExists === undefined
|
|
513
|
+
return this.evalScenarioExists === undefined
|
|
514
|
+
? true
|
|
515
|
+
: this.evalScenarioExists(scenarioId);
|
|
412
516
|
}
|
|
413
517
|
return false;
|
|
414
518
|
}
|
|
@@ -444,41 +548,57 @@ export function createLocalInventoryController(options = {}) {
|
|
|
444
548
|
return new LocalInventoryController(options);
|
|
445
549
|
}
|
|
446
550
|
export function createDisabledInventoryOperatorAuthorizer() {
|
|
447
|
-
return {
|
|
551
|
+
return {
|
|
552
|
+
async authenticate() {
|
|
553
|
+
throw new InventoryError("Inventory operator authorization is not configured.");
|
|
554
|
+
},
|
|
555
|
+
};
|
|
448
556
|
}
|
|
449
557
|
export function createTestInventoryOperatorAuthorizer() {
|
|
450
558
|
const issued = new WeakSet();
|
|
451
559
|
return {
|
|
452
560
|
authorizer: {
|
|
453
561
|
async authenticate(identity) {
|
|
454
|
-
if (identity === null ||
|
|
562
|
+
if (identity === null ||
|
|
563
|
+
typeof identity !== "object" ||
|
|
564
|
+
!issued.has(identity))
|
|
455
565
|
throw new InventoryError("Inventory operator identity is not authenticated.");
|
|
456
566
|
return validateOperator(identity);
|
|
457
|
-
}
|
|
567
|
+
},
|
|
458
568
|
},
|
|
459
569
|
issue(input) {
|
|
460
570
|
const operator = Object.freeze(validateOperator(input));
|
|
461
571
|
issued.add(operator);
|
|
462
572
|
return operator;
|
|
463
|
-
}
|
|
573
|
+
},
|
|
464
574
|
};
|
|
465
575
|
}
|
|
466
576
|
export function formatImportSummary(summary) {
|
|
467
577
|
return `Inventory import ${summary.runId} at ${summary.importedAt} (digest ${summary.digest.slice(0, 16)}...) added ${summary.recordCounts.agents} agents, ${summary.recordCounts.tools} tools, ${summary.recordCounts.identities} identities, ${summary.recordCounts.providers} providers, ${summary.recordCounts.resources} resources, ${summary.recordCounts.sideEffects} side effects, ${summary.recordCounts.credentials} credentials, ${summary.recordCounts.policies} policies; edges created ${summary.edgesCreated}, refreshed ${summary.edgesRefreshed}.`;
|
|
468
578
|
}
|
|
469
579
|
export function formatAttackPathReport(report) {
|
|
470
|
-
const lines = [
|
|
580
|
+
const lines = [
|
|
581
|
+
`Attack paths for agent ${report.agentId}: ${report.paths.length} path(s), ${report.incompletePaths} incomplete`,
|
|
582
|
+
];
|
|
471
583
|
for (const path of report.paths) {
|
|
472
|
-
const chain = path.steps
|
|
473
|
-
|
|
584
|
+
const chain = path.steps
|
|
585
|
+
.map((step) => `${step.kind}:${step.id}`)
|
|
586
|
+
.join(" -> ");
|
|
587
|
+
const edgeNotes = path.edges.length === 0
|
|
588
|
+
? " (no persisted edges)"
|
|
589
|
+
: ` [edges ${path.edges.map((edge) => `${edge.sourceId}->${edge.targetId} (${edge.relation})`).join(", ")}]`;
|
|
474
590
|
lines.push(` ${path.complete ? "complete" : "INCOMPLETE"} ${chain}${edgeNotes}`);
|
|
475
591
|
}
|
|
476
592
|
return lines.join("\n");
|
|
477
593
|
}
|
|
478
594
|
export function formatBlastRadiusReport(report) {
|
|
479
|
-
const lines = [
|
|
595
|
+
const lines = [
|
|
596
|
+
`Blast radius for agent ${report.agentId}: ${report.resources.length} reachable resource(s), ${report.incompletePaths} incomplete path(s)`,
|
|
597
|
+
];
|
|
480
598
|
for (const resource of report.resources) {
|
|
481
|
-
const effects = resource.sideEffects
|
|
599
|
+
const effects = resource.sideEffects
|
|
600
|
+
.map((sideEffect) => `${sideEffect.kind} (${sideEffect.severity})`)
|
|
601
|
+
.join(", ") || "none";
|
|
482
602
|
lines.push(` resource ${resource.resourceId} via provider ${resource.providerId}: ${effects}${resource.sensitive ? " [sensitive]" : ""} (${resource.pathCount} complete path(s))`);
|
|
483
603
|
}
|
|
484
604
|
lines.push(" Rules:");
|
|
@@ -488,7 +608,9 @@ export function formatBlastRadiusReport(report) {
|
|
|
488
608
|
return lines.join("\n");
|
|
489
609
|
}
|
|
490
610
|
export function formatRoiReport(report) {
|
|
491
|
-
const lines = [
|
|
611
|
+
const lines = [
|
|
612
|
+
`ROI report for tenant ${report.tenantId} at ${report.generatedAt} (${report.basis})`,
|
|
613
|
+
];
|
|
492
614
|
lines.push(` Active agents: ${report.activeAgentCount}; production providers: ${report.productionProviderCount}`);
|
|
493
615
|
lines.push(` Incidents: prevented egress attempts ${report.incidents.preventedEgressAttempts}${report.incidents.preventedEgressSources.length === 0 ? " (not measured)" : ""}`);
|
|
494
616
|
lines.push(` Review time: ${report.incidents.reviewTimeMinutes === null ? "not measured" : `${report.incidents.reviewTimeMinutes} minutes`}`);
|
|
@@ -500,7 +622,9 @@ export function formatRoiReport(report) {
|
|
|
500
622
|
return lines.join("\n");
|
|
501
623
|
}
|
|
502
624
|
export function formatRemovalAnalysis(report) {
|
|
503
|
-
const lines = [
|
|
625
|
+
const lines = [
|
|
626
|
+
`Removal analysis for tenant ${report.tenantId} at ${report.generatedAt} (${report.basis})`,
|
|
627
|
+
];
|
|
504
628
|
const coverage = report.coverage;
|
|
505
629
|
lines.push(` Coverage: policy ${coverage.policyCoverageBps} bps, evidence ${coverage.evidenceCoverageBps} bps, gateway ${coverage.gatewayCoverageBps} bps, kill switch ${coverage.killSwitchCoverageBps} bps`);
|
|
506
630
|
lines.push(` Direct credentials: ${coverage.directCredentials}; outside-gateway agents: ${coverage.outsideGatewayAgents}; unowned production: ${coverage.unownedProduction}`);
|
|
@@ -541,20 +665,28 @@ class ImportedRecords {
|
|
|
541
665
|
function validateCrossReferences(state, tenantId, imported) {
|
|
542
666
|
const has = (kind, id) => {
|
|
543
667
|
if (kind === "agent")
|
|
544
|
-
return imported.agentIds.has(id) ||
|
|
668
|
+
return (imported.agentIds.has(id) ||
|
|
669
|
+
state.agents.some((record) => record.tenantId === tenantId && record.agentId === id));
|
|
545
670
|
if (kind === "tool")
|
|
546
|
-
return imported.toolIds.has(id) ||
|
|
671
|
+
return (imported.toolIds.has(id) ||
|
|
672
|
+
state.tools.some((record) => record.tenantId === tenantId && record.toolId === id));
|
|
547
673
|
if (kind === "identity")
|
|
548
|
-
return imported.identityIds.has(id) ||
|
|
674
|
+
return (imported.identityIds.has(id) ||
|
|
675
|
+
state.identities.some((record) => record.tenantId === tenantId && record.identityId === id));
|
|
549
676
|
if (kind === "provider")
|
|
550
|
-
return imported.providerIds.has(id) ||
|
|
677
|
+
return (imported.providerIds.has(id) ||
|
|
678
|
+
state.providers.some((record) => record.tenantId === tenantId && record.providerId === id));
|
|
551
679
|
if (kind === "resource")
|
|
552
|
-
return imported.resourceIds.has(id) ||
|
|
680
|
+
return (imported.resourceIds.has(id) ||
|
|
681
|
+
state.resources.some((record) => record.tenantId === tenantId && record.resourceId === id));
|
|
553
682
|
if (kind === "side_effect")
|
|
554
|
-
return imported.sideEffectIds.has(id) ||
|
|
683
|
+
return (imported.sideEffectIds.has(id) ||
|
|
684
|
+
state.sideEffects.some((record) => record.tenantId === tenantId && record.sideEffectId === id));
|
|
555
685
|
if (kind === "credential")
|
|
556
|
-
return imported.credentialIds.has(id) ||
|
|
557
|
-
|
|
686
|
+
return (imported.credentialIds.has(id) ||
|
|
687
|
+
state.credentials.some((record) => record.tenantId === tenantId && record.credentialId === id));
|
|
688
|
+
return (imported.policyIds.has(id) ||
|
|
689
|
+
state.policies.some((record) => record.tenantId === tenantId && record.policyId === id));
|
|
558
690
|
};
|
|
559
691
|
for (const agent of importedAgents(state, tenantId)) {
|
|
560
692
|
for (const identityId of agent.identityIds)
|
|
@@ -596,25 +728,62 @@ function validateCrossReferences(state, tenantId, imported) {
|
|
|
596
728
|
}
|
|
597
729
|
}
|
|
598
730
|
function importedAgents(state, tenantId) {
|
|
599
|
-
return state.agents
|
|
731
|
+
return state.agents
|
|
732
|
+
.filter((record) => record.tenantId === tenantId)
|
|
733
|
+
.map((record) => ({
|
|
734
|
+
agentId: record.agentId,
|
|
735
|
+
identityIds: record.identityIds,
|
|
736
|
+
}));
|
|
600
737
|
}
|
|
601
738
|
function importedIdentities(state, tenantId) {
|
|
602
|
-
return state.identities
|
|
739
|
+
return state.identities
|
|
740
|
+
.filter((record) => record.tenantId === tenantId)
|
|
741
|
+
.map((record) => ({
|
|
742
|
+
identityId: record.identityId,
|
|
743
|
+
toolIds: record.toolIds,
|
|
744
|
+
}));
|
|
603
745
|
}
|
|
604
746
|
function importedTools(state, tenantId) {
|
|
605
|
-
return state.tools
|
|
747
|
+
return state.tools
|
|
748
|
+
.filter((record) => record.tenantId === tenantId)
|
|
749
|
+
.map((record) => ({
|
|
750
|
+
toolId: record.toolId,
|
|
751
|
+
providerId: record.providerId,
|
|
752
|
+
credentialIds: record.credentialIds,
|
|
753
|
+
}));
|
|
606
754
|
}
|
|
607
755
|
function importedProviders(state, tenantId) {
|
|
608
|
-
return state.providers
|
|
756
|
+
return state.providers
|
|
757
|
+
.filter((record) => record.tenantId === tenantId)
|
|
758
|
+
.map((record) => ({
|
|
759
|
+
providerId: record.providerId,
|
|
760
|
+
resourceIds: record.resourceIds,
|
|
761
|
+
}));
|
|
609
762
|
}
|
|
610
763
|
function importedResources(state, tenantId) {
|
|
611
|
-
return state.resources
|
|
764
|
+
return state.resources
|
|
765
|
+
.filter((record) => record.tenantId === tenantId)
|
|
766
|
+
.map((record) => ({
|
|
767
|
+
resourceId: record.resourceId,
|
|
768
|
+
sideEffectIds: record.sideEffectIds,
|
|
769
|
+
}));
|
|
612
770
|
}
|
|
613
771
|
function importedSideEffects(state, tenantId) {
|
|
614
|
-
return state.sideEffects
|
|
772
|
+
return state.sideEffects
|
|
773
|
+
.filter((record) => record.tenantId === tenantId)
|
|
774
|
+
.map((record) => ({
|
|
775
|
+
sideEffectId: record.sideEffectId,
|
|
776
|
+
resourceId: record.resourceId,
|
|
777
|
+
}));
|
|
615
778
|
}
|
|
616
779
|
function importedCredentials(state, tenantId) {
|
|
617
|
-
return state.credentials
|
|
780
|
+
return state.credentials
|
|
781
|
+
.filter((record) => record.tenantId === tenantId)
|
|
782
|
+
.map((record) => ({
|
|
783
|
+
credentialId: record.credentialId,
|
|
784
|
+
providerId: record.providerId,
|
|
785
|
+
toolIds: record.toolIds,
|
|
786
|
+
}));
|
|
618
787
|
}
|
|
619
788
|
function upsertAgent(state, tenantId, input, payload, importedBy, now) {
|
|
620
789
|
const record = {
|
|
@@ -624,7 +793,9 @@ function upsertAgent(state, tenantId, input, payload, importedBy, now) {
|
|
|
624
793
|
agentId: input.agentId,
|
|
625
794
|
name: input.name,
|
|
626
795
|
ownerId: input.ownerId === undefined ? null : input.ownerId,
|
|
627
|
-
...(input.businessPurpose === undefined
|
|
796
|
+
...(input.businessPurpose === undefined
|
|
797
|
+
? {}
|
|
798
|
+
: { businessPurpose: input.businessPurpose }),
|
|
628
799
|
...(input.model === undefined ? {} : { model: input.model }),
|
|
629
800
|
...(input.runtime === undefined ? {} : { runtime: input.runtime }),
|
|
630
801
|
...(input.version === undefined ? {} : { version: input.version }),
|
|
@@ -632,7 +803,7 @@ function upsertAgent(state, tenantId, input, payload, importedBy, now) {
|
|
|
632
803
|
environmentIds: input.environmentIds,
|
|
633
804
|
gatewayManaged: input.gatewayManaged,
|
|
634
805
|
killSwitchEnabled: input.killSwitchEnabled,
|
|
635
|
-
lastEvidenceAt: input.lastEvidenceAt === undefined ? null : input.lastEvidenceAt
|
|
806
|
+
lastEvidenceAt: input.lastEvidenceAt === undefined ? null : input.lastEvidenceAt,
|
|
636
807
|
};
|
|
637
808
|
merge(state, "agent", input.agentId, tenantId, record, payload.source, importedBy, now);
|
|
638
809
|
}
|
|
@@ -645,7 +816,7 @@ function upsertTool(state, tenantId, input, payload, importedBy, now) {
|
|
|
645
816
|
kind: input.kind,
|
|
646
817
|
providerId: input.providerId === undefined ? null : input.providerId,
|
|
647
818
|
requiredScopes: input.requiredScopes,
|
|
648
|
-
credentialIds: input.credentialIds
|
|
819
|
+
credentialIds: input.credentialIds,
|
|
649
820
|
};
|
|
650
821
|
merge(state, "tool", input.toolId, tenantId, record, payload.source, importedBy, now);
|
|
651
822
|
}
|
|
@@ -660,7 +831,7 @@ function upsertIdentity(state, tenantId, input, payload, importedBy, now) {
|
|
|
660
831
|
agentId: input.agentId === undefined ? null : input.agentId,
|
|
661
832
|
toolIds: input.toolIds,
|
|
662
833
|
environmentIds: input.environmentIds,
|
|
663
|
-
scopes: input.scopes
|
|
834
|
+
scopes: input.scopes,
|
|
664
835
|
};
|
|
665
836
|
merge(state, "identity", input.identityId, tenantId, record, payload.source, importedBy, now);
|
|
666
837
|
}
|
|
@@ -676,7 +847,7 @@ function upsertProvider(state, tenantId, input, payload, importedBy, now) {
|
|
|
676
847
|
environmentIds: input.environmentIds,
|
|
677
848
|
resourceIds: input.resourceIds,
|
|
678
849
|
gatewayBound: input.gatewayBound,
|
|
679
|
-
killSwitchApplied: input.killSwitchApplied
|
|
850
|
+
killSwitchApplied: input.killSwitchApplied,
|
|
680
851
|
};
|
|
681
852
|
merge(state, "provider", input.providerId, tenantId, record, payload.source, importedBy, now);
|
|
682
853
|
}
|
|
@@ -692,8 +863,10 @@ function upsertResource(state, tenantId, input, payload, importedBy, now) {
|
|
|
692
863
|
environmentIds: input.environmentIds,
|
|
693
864
|
sensitive: input.sensitive,
|
|
694
865
|
ownerId: input.ownerId === undefined ? null : input.ownerId,
|
|
695
|
-
...(input.businessPurpose === undefined
|
|
696
|
-
|
|
866
|
+
...(input.businessPurpose === undefined
|
|
867
|
+
? {}
|
|
868
|
+
: { businessPurpose: input.businessPurpose }),
|
|
869
|
+
sideEffectIds: input.sideEffectIds,
|
|
697
870
|
};
|
|
698
871
|
merge(state, "resource", input.resourceId, tenantId, record, payload.source, importedBy, now);
|
|
699
872
|
}
|
|
@@ -704,7 +877,7 @@ function upsertSideEffect(state, tenantId, input, payload, importedBy, now) {
|
|
|
704
877
|
sideEffectId: input.sideEffectId,
|
|
705
878
|
resourceId: input.resourceId,
|
|
706
879
|
kind: input.kind,
|
|
707
|
-
severity: input.severity
|
|
880
|
+
severity: input.severity,
|
|
708
881
|
};
|
|
709
882
|
merge(state, "side_effect", input.sideEffectId, tenantId, record, payload.source, importedBy, now);
|
|
710
883
|
}
|
|
@@ -724,7 +897,7 @@ function upsertCredential(state, tenantId, input, payload, importedBy, now) {
|
|
|
724
897
|
issuedAt: input.issuedAt,
|
|
725
898
|
lastUsedAt: input.lastUsedAt === undefined ? null : input.lastUsedAt,
|
|
726
899
|
...(input.rotatedAt === undefined ? {} : { rotatedAt: input.rotatedAt }),
|
|
727
|
-
gatewayBound: input.gatewayBound
|
|
900
|
+
gatewayBound: input.gatewayBound,
|
|
728
901
|
};
|
|
729
902
|
merge(state, "credential", input.credentialId, tenantId, record, payload.source, importedBy, now);
|
|
730
903
|
}
|
|
@@ -737,7 +910,7 @@ function upsertPolicy(state, tenantId, input, payload, importedBy, now) {
|
|
|
737
910
|
name: input.name,
|
|
738
911
|
version: input.version,
|
|
739
912
|
hash: input.hash,
|
|
740
|
-
environmentIds: input.environmentIds
|
|
913
|
+
environmentIds: input.environmentIds,
|
|
741
914
|
};
|
|
742
915
|
merge(state, "policy", input.policyId, tenantId, record, payload.source, importedBy, now);
|
|
743
916
|
}
|
|
@@ -754,7 +927,10 @@ function merge(state, kind, id, tenantId, record, source, importedBy, now) {
|
|
|
754
927
|
}
|
|
755
928
|
else {
|
|
756
929
|
const prior = list[index];
|
|
757
|
-
const mergedFreshness = {
|
|
930
|
+
const mergedFreshness = {
|
|
931
|
+
firstSeenAt: prior.freshness?.firstSeenAt ?? now,
|
|
932
|
+
lastSeenAt: now,
|
|
933
|
+
};
|
|
758
934
|
list[index] = { ...stored, freshness: mergedFreshness, provenance };
|
|
759
935
|
}
|
|
760
936
|
}
|
|
@@ -823,11 +999,22 @@ function upsertEdges(state, tenantId, upserted, payload, importedBy, now, before
|
|
|
823
999
|
const edgeId = edgeIdFor(sourceKind, sourceId, targetKind, targetId, relation);
|
|
824
1000
|
const prior = before.get(edgeId);
|
|
825
1001
|
if (prior === undefined) {
|
|
826
|
-
state.edges.push({
|
|
1002
|
+
state.edges.push({
|
|
1003
|
+
tenantId,
|
|
1004
|
+
edgeId,
|
|
1005
|
+
sourceKind,
|
|
1006
|
+
sourceId,
|
|
1007
|
+
targetKind,
|
|
1008
|
+
targetId,
|
|
1009
|
+
relation,
|
|
1010
|
+
provenance,
|
|
1011
|
+
firstSeenAt: now,
|
|
1012
|
+
lastSeenAt: now,
|
|
1013
|
+
});
|
|
827
1014
|
edgesCreated += 1;
|
|
828
1015
|
}
|
|
829
1016
|
else {
|
|
830
|
-
state.edges = state.edges.map((edge) =>
|
|
1017
|
+
state.edges = state.edges.map((edge) => edge.edgeId === edgeId ? { ...edge, lastSeenAt: now } : edge);
|
|
831
1018
|
edgesRefreshed += 1;
|
|
832
1019
|
}
|
|
833
1020
|
};
|
|
@@ -870,7 +1057,20 @@ function upsertEdges(state, tenantId, upserted, payload, importedBy, now, before
|
|
|
870
1057
|
apply("resource", resource.resourceId, "side_effect", sideEffectId, "permits");
|
|
871
1058
|
}
|
|
872
1059
|
}
|
|
873
|
-
return {
|
|
1060
|
+
return {
|
|
1061
|
+
recordCounts: {
|
|
1062
|
+
agents: 0,
|
|
1063
|
+
tools: 0,
|
|
1064
|
+
identities: 0,
|
|
1065
|
+
providers: 0,
|
|
1066
|
+
resources: 0,
|
|
1067
|
+
sideEffects: 0,
|
|
1068
|
+
credentials: 0,
|
|
1069
|
+
policies: 0,
|
|
1070
|
+
},
|
|
1071
|
+
edgesCreated,
|
|
1072
|
+
edgesRefreshed,
|
|
1073
|
+
};
|
|
874
1074
|
}
|
|
875
1075
|
function snapshotFor(state, tenantId, now, freshnessDays) {
|
|
876
1076
|
return {
|
|
@@ -889,7 +1089,7 @@ function snapshotFor(state, tenantId, now, freshnessDays) {
|
|
|
889
1089
|
findings: clone(state.findings.filter((record) => record.tenantId === tenantId)),
|
|
890
1090
|
remediations: clone(state.remediations.filter((record) => record.tenantId === tenantId)),
|
|
891
1091
|
sources: clone(state.sources.filter((record) => record.tenantId === tenantId)),
|
|
892
|
-
importRuns: clone(state.importRuns.filter((record) => record.tenantId === tenantId))
|
|
1092
|
+
importRuns: clone(state.importRuns.filter((record) => record.tenantId === tenantId)),
|
|
893
1093
|
};
|
|
894
1094
|
}
|
|
895
1095
|
function applyRemediationEffect(state, tenantId, remediation, now) {
|
|
@@ -898,25 +1098,29 @@ function applyRemediationEffect(state, tenantId, remediation, now) {
|
|
|
898
1098
|
if (result === undefined || result.ownerId === undefined)
|
|
899
1099
|
throw new InventoryError("assign_owner remediation requires an owner id.");
|
|
900
1100
|
if (remediation.targetKind === "agent") {
|
|
901
|
-
const record = state.agents.find((candidate) => candidate.tenantId === tenantId &&
|
|
1101
|
+
const record = state.agents.find((candidate) => candidate.tenantId === tenantId &&
|
|
1102
|
+
candidate.agentId === remediation.targetId);
|
|
902
1103
|
if (record === undefined)
|
|
903
1104
|
throw new InventoryError("Remediation target agent does not exist.");
|
|
904
1105
|
record.ownerId = result.ownerId;
|
|
905
1106
|
}
|
|
906
1107
|
else if (remediation.targetKind === "provider") {
|
|
907
|
-
const record = state.providers.find((candidate) => candidate.tenantId === tenantId &&
|
|
1108
|
+
const record = state.providers.find((candidate) => candidate.tenantId === tenantId &&
|
|
1109
|
+
candidate.providerId === remediation.targetId);
|
|
908
1110
|
if (record === undefined)
|
|
909
1111
|
throw new InventoryError("Remediation target provider does not exist.");
|
|
910
1112
|
record.ownerId = result.ownerId;
|
|
911
1113
|
}
|
|
912
1114
|
else if (remediation.targetKind === "resource") {
|
|
913
|
-
const record = state.resources.find((candidate) => candidate.tenantId === tenantId &&
|
|
1115
|
+
const record = state.resources.find((candidate) => candidate.tenantId === tenantId &&
|
|
1116
|
+
candidate.resourceId === remediation.targetId);
|
|
914
1117
|
if (record === undefined)
|
|
915
1118
|
throw new InventoryError("Remediation target resource does not exist.");
|
|
916
1119
|
record.ownerId = result.ownerId;
|
|
917
1120
|
}
|
|
918
1121
|
else if (remediation.targetKind === "credential") {
|
|
919
|
-
const record = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
1122
|
+
const record = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
1123
|
+
candidate.credentialId === remediation.targetId);
|
|
920
1124
|
if (record === undefined)
|
|
921
1125
|
throw new InventoryError("Remediation target credential does not exist.");
|
|
922
1126
|
record.ownerId = result.ownerId;
|
|
@@ -930,7 +1134,8 @@ function applyRemediationEffect(state, tenantId, remediation, now) {
|
|
|
930
1134
|
throw new InventoryError("reduce_scope remediation only supports credential targets.");
|
|
931
1135
|
if (result === undefined || result.reducedScopes === undefined)
|
|
932
1136
|
throw new InventoryError("reduce_scope remediation requires the reduced scope list.");
|
|
933
|
-
const record = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
1137
|
+
const record = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
1138
|
+
candidate.credentialId === remediation.targetId);
|
|
934
1139
|
if (record === undefined)
|
|
935
1140
|
throw new InventoryError("Remediation target credential does not exist.");
|
|
936
1141
|
if (record.status !== "active")
|
|
@@ -947,7 +1152,8 @@ function applyRemediationEffect(state, tenantId, remediation, now) {
|
|
|
947
1152
|
else if (remediation.kind === "revoke") {
|
|
948
1153
|
if (remediation.targetKind !== "credential")
|
|
949
1154
|
throw new InventoryError("revoke remediation only supports credential targets.");
|
|
950
|
-
const record = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
1155
|
+
const record = state.credentials.find((candidate) => candidate.tenantId === tenantId &&
|
|
1156
|
+
candidate.credentialId === remediation.targetId);
|
|
951
1157
|
if (record === undefined)
|
|
952
1158
|
throw new InventoryError("Remediation target credential does not exist.");
|
|
953
1159
|
record.status = "revoked";
|
|
@@ -955,7 +1161,8 @@ function applyRemediationEffect(state, tenantId, remediation, now) {
|
|
|
955
1161
|
else if (remediation.kind === "onboard_through_gateway") {
|
|
956
1162
|
if (remediation.targetKind !== "agent")
|
|
957
1163
|
throw new InventoryError("onboard_through_gateway remediation only supports agent targets.");
|
|
958
|
-
const record = state.agents.find((candidate) => candidate.tenantId === tenantId &&
|
|
1164
|
+
const record = state.agents.find((candidate) => candidate.tenantId === tenantId &&
|
|
1165
|
+
candidate.agentId === remediation.targetId);
|
|
959
1166
|
if (record === undefined)
|
|
960
1167
|
throw new InventoryError("Remediation target agent does not exist.");
|
|
961
1168
|
record.gatewayManaged = true;
|
|
@@ -966,7 +1173,8 @@ function applyRemediationEffect(state, tenantId, remediation, now) {
|
|
|
966
1173
|
throw new InventoryError("create_eval remediation only supports agent targets.");
|
|
967
1174
|
if (result === undefined || result.evalScenarioId === undefined)
|
|
968
1175
|
throw new InventoryError("create_eval remediation requires an eval scenario id.");
|
|
969
|
-
if (!state.agents.some((record) => record.tenantId === tenantId &&
|
|
1176
|
+
if (!state.agents.some((record) => record.tenantId === tenantId &&
|
|
1177
|
+
record.agentId === remediation.targetId))
|
|
970
1178
|
throw new InventoryError("Remediation target agent does not exist.");
|
|
971
1179
|
}
|
|
972
1180
|
}
|
|
@@ -974,25 +1182,54 @@ function remediationResult(proposal, proposedBy) {
|
|
|
974
1182
|
return {
|
|
975
1183
|
description: `Proposed by ${proposedBy}`,
|
|
976
1184
|
...(proposal.ownerId === undefined ? {} : { ownerId: proposal.ownerId }),
|
|
977
|
-
...(proposal.reducedScopes === undefined
|
|
978
|
-
|
|
1185
|
+
...(proposal.reducedScopes === undefined
|
|
1186
|
+
? {}
|
|
1187
|
+
: { reducedScopes: proposal.reducedScopes }),
|
|
1188
|
+
...(proposal.evalScenarioId === undefined
|
|
1189
|
+
? {}
|
|
1190
|
+
: { evalScenarioId: proposal.evalScenarioId }),
|
|
979
1191
|
};
|
|
980
1192
|
}
|
|
981
1193
|
function validateRemediationProposal(value) {
|
|
982
1194
|
const proposal = object(value, "Remediation proposal must be an object.");
|
|
983
|
-
exactKeys(proposal, [
|
|
1195
|
+
exactKeys(proposal, [
|
|
1196
|
+
"findingId",
|
|
1197
|
+
"kind",
|
|
1198
|
+
"targetKind",
|
|
1199
|
+
"targetId",
|
|
1200
|
+
"rationale",
|
|
1201
|
+
"ownerId",
|
|
1202
|
+
"reducedScopes",
|
|
1203
|
+
"evalScenarioId",
|
|
1204
|
+
], "remediation proposal", ["ownerId", "reducedScopes", "evalScenarioId"]);
|
|
984
1205
|
const kind = proposal.kind;
|
|
985
|
-
if (![
|
|
1206
|
+
if (![
|
|
1207
|
+
"assign_owner",
|
|
1208
|
+
"reduce_scope",
|
|
1209
|
+
"revoke",
|
|
1210
|
+
"onboard_through_gateway",
|
|
1211
|
+
"create_eval",
|
|
1212
|
+
].includes(kind))
|
|
986
1213
|
throw new InventoryError("Remediation kind is invalid.");
|
|
987
1214
|
const targetKind = proposal.targetKind;
|
|
988
|
-
if (![
|
|
1215
|
+
if (![
|
|
1216
|
+
"agent",
|
|
1217
|
+
"identity",
|
|
1218
|
+
"tool",
|
|
1219
|
+
"provider",
|
|
1220
|
+
"resource",
|
|
1221
|
+
"side_effect",
|
|
1222
|
+
"credential",
|
|
1223
|
+
"environment",
|
|
1224
|
+
"policy",
|
|
1225
|
+
].includes(targetKind))
|
|
989
1226
|
throw new InventoryError("Remediation target kind is invalid.");
|
|
990
1227
|
const result = {
|
|
991
1228
|
findingId: safeIdentifier(proposal.findingId, "Remediation finding id"),
|
|
992
1229
|
kind: kind,
|
|
993
1230
|
targetKind: targetKind,
|
|
994
1231
|
targetId: safeIdentifier(proposal.targetId, "Remediation target id"),
|
|
995
|
-
rationale: safeText(proposal.rationale, "Remediation rationale", 400)
|
|
1232
|
+
rationale: safeText(proposal.rationale, "Remediation rationale", 400),
|
|
996
1233
|
};
|
|
997
1234
|
if (proposal.ownerId !== undefined)
|
|
998
1235
|
result.ownerId = safeIdentifier(proposal.ownerId, "Remediation owner id");
|
|
@@ -1009,21 +1246,30 @@ function validateRemediationProposal(value) {
|
|
|
1009
1246
|
return result;
|
|
1010
1247
|
}
|
|
1011
1248
|
function safeIdentifier(value, label) {
|
|
1012
|
-
if (typeof value !== "string" ||
|
|
1249
|
+
if (typeof value !== "string" ||
|
|
1250
|
+
!/^[a-z0-9][a-z0-9._-]{0,127}$/.test(value) ||
|
|
1251
|
+
sanitizeSecretString(value) !== value)
|
|
1013
1252
|
throw new InventoryError(`${label} must be a safe identifier.`);
|
|
1014
1253
|
return value;
|
|
1015
1254
|
}
|
|
1016
1255
|
function safeScopes(value, label) {
|
|
1017
|
-
if (!Array.isArray(value) ||
|
|
1256
|
+
if (!Array.isArray(value) ||
|
|
1257
|
+
value.length > INVENTORY_LIMITS.maxScopesPerRecord)
|
|
1018
1258
|
throw new InventoryError(`${label} is invalid.`);
|
|
1019
1259
|
return value.map((entry) => {
|
|
1020
|
-
if (typeof entry !== "string" ||
|
|
1260
|
+
if (typeof entry !== "string" ||
|
|
1261
|
+
!/^[a-z0-9][a-z0-9._:\-]{0,127}$/.test(entry) ||
|
|
1262
|
+
sanitizeSecretString(entry) !== entry)
|
|
1021
1263
|
throw new InventoryError(`${label} must be safe scope identifiers.`);
|
|
1022
1264
|
return entry;
|
|
1023
1265
|
});
|
|
1024
1266
|
}
|
|
1025
1267
|
function safeText(value, label, max) {
|
|
1026
|
-
if (typeof value !== "string" ||
|
|
1268
|
+
if (typeof value !== "string" ||
|
|
1269
|
+
value.trim() === "" ||
|
|
1270
|
+
value.length > max ||
|
|
1271
|
+
/[\u0000-\u001f]/.test(value) ||
|
|
1272
|
+
sanitizeSecretString(value) !== value)
|
|
1027
1273
|
throw new InventoryError(`${label} is invalid.`);
|
|
1028
1274
|
return value.trim();
|
|
1029
1275
|
}
|
|
@@ -1034,7 +1280,8 @@ function object(value, message) {
|
|
|
1034
1280
|
}
|
|
1035
1281
|
function exactKeys(value, keys, label, optional = []) {
|
|
1036
1282
|
for (const key of Object.keys(value)) {
|
|
1037
|
-
if (!keys.includes(key) ||
|
|
1283
|
+
if (!keys.includes(key) ||
|
|
1284
|
+
(value[key] === undefined && !optional.includes(key)))
|
|
1038
1285
|
throw new InventoryError(`${label} contains unsupported field: ${key}`);
|
|
1039
1286
|
}
|
|
1040
1287
|
}
|