@prism-d1/cli 1.0.94 → 1.0.96
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/dist/src/commands/bedrock-protection/scan-account.d.ts +45 -0
- package/dist/src/commands/bedrock-protection/scan-account.d.ts.map +1 -0
- package/dist/src/commands/bedrock-protection/scan-account.js +107 -0
- package/dist/src/commands/bedrock-protection/scan-account.js.map +1 -0
- package/dist/src/commands/bedrock-protection/scan-org.d.ts +23 -0
- package/dist/src/commands/bedrock-protection/scan-org.d.ts.map +1 -0
- package/dist/src/commands/bedrock-protection/scan-org.js +328 -0
- package/dist/src/commands/bedrock-protection/scan-org.js.map +1 -0
- package/dist/src/commands/bedrock-protection/scan-repo.d.ts +16 -0
- package/dist/src/commands/bedrock-protection/scan-repo.d.ts.map +1 -0
- package/dist/src/commands/bedrock-protection/scan-repo.js +397 -0
- package/dist/src/commands/bedrock-protection/scan-repo.js.map +1 -0
- package/dist/src/utils/audit-bedrock.d.ts +25 -0
- package/dist/src/utils/audit-bedrock.d.ts.map +1 -0
- package/dist/src/utils/audit-bedrock.js +331 -0
- package/dist/src/utils/audit-bedrock.js.map +1 -0
- package/dist/src/utils/audit-iam.d.ts +7 -0
- package/dist/src/utils/audit-iam.d.ts.map +1 -0
- package/dist/src/utils/audit-iam.js +239 -0
- package/dist/src/utils/audit-iam.js.map +1 -0
- package/dist/src/utils/audit-org.d.ts +80 -0
- package/dist/src/utils/audit-org.d.ts.map +1 -0
- package/dist/src/utils/audit-org.js +304 -0
- package/dist/src/utils/audit-org.js.map +1 -0
- package/dist/src/utils/audit.d.ts +61 -0
- package/dist/src/utils/audit.d.ts.map +1 -0
- package/dist/src/utils/audit.js +90 -0
- package/dist/src/utils/audit.js.map +1 -0
- package/dist/src/utils/aws.d.ts +69 -0
- package/dist/src/utils/aws.d.ts.map +1 -0
- package/dist/src/utils/aws.js +103 -0
- package/dist/src/utils/aws.js.map +1 -0
- package/dist/src/utils/exec.d.ts +12 -1
- package/dist/src/utils/exec.d.ts.map +1 -1
- package/dist/src/utils/exec.js +23 -2
- package/dist/src/utils/exec.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { aws } from './aws.js';
|
|
2
|
+
/**
|
|
3
|
+
* Audit Bedrock blast-radius controls -- what bounds runaway model spend, how
|
|
4
|
+
* fast you find out, and whether you can attribute it afterwards.
|
|
5
|
+
*
|
|
6
|
+
* These checks are composed by `bedrock-protection scan-account` alongside the
|
|
7
|
+
* IAM hygiene checks in audit-iam.ts. Repository credential exposure is the
|
|
8
|
+
* third side, covered by `bedrock-protection scan-repo`.
|
|
9
|
+
*
|
|
10
|
+
* Audit-only: every AWS call here is a read.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Bedrock spend lands on TWO billing surfaces, and a filter scoped to only the
|
|
14
|
+
* first silently misses the second:
|
|
15
|
+
*
|
|
16
|
+
* 1. `Amazon Bedrock*` services -- first-party models, AgentCore, Knowledge
|
|
17
|
+
* Bases, Guardrails.
|
|
18
|
+
* 2. `AWS Marketplace` -- every third-party model, billed as a subscription
|
|
19
|
+
* line item named e.g. "Claude Opus 5 (Amazon Bedrock Edition)".
|
|
20
|
+
*
|
|
21
|
+
* So a budget filtered on Amazon Bedrock alone reports nothing while an attacker
|
|
22
|
+
* burns Opus -- both the priciest option and the likeliest target. The two
|
|
23
|
+
* surfaces are therefore separate checks below, and a budget covering only one
|
|
24
|
+
* does not clear the other.
|
|
25
|
+
*/
|
|
26
|
+
const BEDROCK_HINTS = ['bedrock'];
|
|
27
|
+
const MARKETPLACE_HINTS = ['marketplace'];
|
|
28
|
+
function budgetCovers(budget, hints) {
|
|
29
|
+
// Older budgets carry CostFilters (dimension -> values). Newer ones carry
|
|
30
|
+
// FilterExpression, a nested tree. Rather than pretend to fully interpret an
|
|
31
|
+
// expression tree, match its serialised form -- and return 'unknown' when
|
|
32
|
+
// neither field is present, because an unrecognised budget must never be
|
|
33
|
+
// counted as coverage.
|
|
34
|
+
const filters = budget?.CostFilters;
|
|
35
|
+
if (filters && Object.keys(filters).length > 0) {
|
|
36
|
+
const blob = JSON.stringify(filters).toLowerCase();
|
|
37
|
+
return hints.some((h) => blob.includes(h)) ? 'yes' : 'no';
|
|
38
|
+
}
|
|
39
|
+
if (budget?.FilterExpression) {
|
|
40
|
+
const blob = JSON.stringify(budget.FilterExpression).toLowerCase();
|
|
41
|
+
return hints.some((h) => blob.includes(h)) ? 'yes' : 'no';
|
|
42
|
+
}
|
|
43
|
+
// No filter at all means the budget covers total account spend. That bounds
|
|
44
|
+
// Bedrock, but only bluntly -- it will not fire until everything else has
|
|
45
|
+
// grown too.
|
|
46
|
+
return 'unknown';
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Only a COST budget bounds spend.
|
|
50
|
+
*
|
|
51
|
+
* BudgetType is one of USAGE, COST, RI_UTILIZATION, RI_COVERAGE,
|
|
52
|
+
* SAVINGS_PLANS_UTILIZATION or SAVINGS_PLANS_COVERAGE. A USAGE budget filtered on
|
|
53
|
+
* Bedrock tracks request counts, and an RI/SP budget tracks commitment
|
|
54
|
+
* efficiency -- neither caps a dollar figure. Treating any of them as coverage
|
|
55
|
+
* would report a spend ceiling that does not exist, which is worse than
|
|
56
|
+
* reporting none because it stops the reader looking.
|
|
57
|
+
*/
|
|
58
|
+
function isCostBudget(budget) {
|
|
59
|
+
return budget?.BudgetType === 'COST';
|
|
60
|
+
}
|
|
61
|
+
export function auditBudgets(target, accountId) {
|
|
62
|
+
// describe-budgets requires an explicit account id, and returns an EMPTY body
|
|
63
|
+
// -- not {"Budgets": []} -- when no budgets exist.
|
|
64
|
+
const bud = aws(['budgets', 'describe-budgets', '--account-id', accountId], target);
|
|
65
|
+
if (!bud.ok) {
|
|
66
|
+
return { coveringBudgets: [], findings: [
|
|
67
|
+
['bedrock-budget', 'Budget covering Bedrock services'],
|
|
68
|
+
['marketplace-budget', 'Budget covering Marketplace (Bedrock Edition) models'],
|
|
69
|
+
].map(([id, title]) => ({
|
|
70
|
+
id, category: 'budget', title,
|
|
71
|
+
status: 'INDETERMINATE', severity: 'HIGH',
|
|
72
|
+
detail: `Could not list budgets (${bud.errorCode || 'unknown error'}).`,
|
|
73
|
+
remediation: 'Grant budgets:DescribeBudgets and re-run.',
|
|
74
|
+
})) };
|
|
75
|
+
}
|
|
76
|
+
const budgets = bud.json?.Budgets ?? [];
|
|
77
|
+
// Coverage requires BOTH a matching filter and BudgetType === COST.
|
|
78
|
+
const covering = (hints) => budgets.filter((b) => budgetCovers(b, hints) === 'yes' && isCostBudget(b)).map((b) => b.BudgetName);
|
|
79
|
+
// Matching filter but the wrong budget type: named explicitly, because from
|
|
80
|
+
// the console it looks like a Bedrock budget while capping no dollar figure.
|
|
81
|
+
const wrongType = (hints) => budgets.filter((b) => budgetCovers(b, hints) === 'yes' && !isCostBudget(b))
|
|
82
|
+
.map((b) => `${b.BudgetName} (BudgetType=${b.BudgetType})`);
|
|
83
|
+
const untargeted = budgets
|
|
84
|
+
.filter((b) => budgetCovers(b, BEDROCK_HINTS) === 'unknown' && isCostBudget(b))
|
|
85
|
+
.map((b) => b.BudgetName);
|
|
86
|
+
const bedrock = covering(BEDROCK_HINTS);
|
|
87
|
+
const marketplace = covering(MARKETPLACE_HINTS);
|
|
88
|
+
const bedrockWrongType = wrongType(BEDROCK_HINTS);
|
|
89
|
+
const marketplaceWrongType = wrongType(MARKETPLACE_HINTS);
|
|
90
|
+
return { coveringBudgets: [...new Set([...bedrock, ...marketplace])], findings: [
|
|
91
|
+
{
|
|
92
|
+
id: 'bedrock-budget', category: 'budget', title: 'Budget covering Bedrock services',
|
|
93
|
+
status: bedrock.length ? 'PASS' : 'FAIL', severity: 'HIGH',
|
|
94
|
+
detail: bedrock.length
|
|
95
|
+
? `Covered by: ${bedrock.join(', ')}.`
|
|
96
|
+
: budgets.length === 0
|
|
97
|
+
? 'No budgets exist in this account, so runaway Bedrock spend has no ceiling and raises no alert.'
|
|
98
|
+
: bedrockWrongType.length
|
|
99
|
+
? `${bedrockWrongType.join(', ')} filters on Bedrock but is not a COST budget, so it caps no dollar figure — it looks like a Bedrock budget in the console while bounding nothing.`
|
|
100
|
+
: `${budgets.length} budget(s) exist but none is a COST budget filtering on a Bedrock service${untargeted.length ? ` (${untargeted.length} cover total account spend, which bounds Bedrock only indirectly)` : ''}.`,
|
|
101
|
+
remediation: bedrock.length ? undefined : 'Create a monthly budget filtered on Service contains "Bedrock", alerting at 50/80/100% to SNS.',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'marketplace-budget', category: 'budget', title: 'Budget covering Marketplace (Bedrock Edition) models',
|
|
105
|
+
status: marketplace.length ? 'PASS' : 'FAIL', severity: 'HIGH',
|
|
106
|
+
detail: marketplace.length
|
|
107
|
+
? `Covered by: ${marketplace.join(', ')}.`
|
|
108
|
+
: (marketplaceWrongType.length ? `${marketplaceWrongType.join(', ')} filters on Marketplace but is not a COST budget, so it caps no dollar figure. ` : '')
|
|
109
|
+
+ 'No COST budget filters on AWS Marketplace. Third-party models invoked through Bedrock — Anthropic, Meta, Mistral, Cohere — bill as Marketplace subscription line items named "(Amazon Bedrock Edition)", NOT under Amazon Bedrock. A Bedrock-only budget misses them entirely, which is the most expensive blind spot here because Opus-class models are both the priciest and the likeliest target.',
|
|
110
|
+
remediation: marketplace.length ? undefined : 'Add a second COST budget filtered on Service = AWS Marketplace, or define a Cost Category grouping both surfaces and budget on that.',
|
|
111
|
+
},
|
|
112
|
+
] };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A budget that alerts nobody is a record, not a control.
|
|
116
|
+
*
|
|
117
|
+
* `describe-budgets` says nothing about whether anyone is told when a threshold
|
|
118
|
+
* is crossed -- notifications and their subscribers are separate resources. A
|
|
119
|
+
* budget created without them still shows a spend bar in the console and still
|
|
120
|
+
* reports as "a Bedrock budget exists", while nothing reaches a human. For
|
|
121
|
+
* LLMjacking specifically, being told is the entire value: the spend has already
|
|
122
|
+
* happened by the time anyone opens the Billing console.
|
|
123
|
+
*
|
|
124
|
+
* `DescribeBudgetNotificationsForAccount` returns notifications for every budget
|
|
125
|
+
* in ONE call. Subscribers need a call per notification, which is why this is
|
|
126
|
+
* scoped to the budgets that actually cover Bedrock rather than all of them.
|
|
127
|
+
*/
|
|
128
|
+
export function auditBudgetAlerting(target, accountId, coveringBudgets) {
|
|
129
|
+
const id = 'budget-alerting';
|
|
130
|
+
const title = 'Bedrock budgets actually notify someone';
|
|
131
|
+
if (coveringBudgets.length === 0) {
|
|
132
|
+
// Nothing to evaluate. Not a PASS -- no alerting was established -- and not
|
|
133
|
+
// a duplicate FAIL, since bedrock-budget/marketplace-budget already report
|
|
134
|
+
// the absence.
|
|
135
|
+
return {
|
|
136
|
+
id, category: 'budget', title,
|
|
137
|
+
status: 'INDETERMINATE', severity: 'HIGH',
|
|
138
|
+
detail: 'No COST budget covers Bedrock or Marketplace, so there is no budget whose alerting could be checked (see bedrock-budget and marketplace-budget above).',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const all = aws(['budgets', 'describe-budget-notifications-for-account', '--account-id', accountId], target);
|
|
142
|
+
if (!all.ok) {
|
|
143
|
+
return {
|
|
144
|
+
id, category: 'budget', title,
|
|
145
|
+
status: 'INDETERMINATE', severity: 'HIGH',
|
|
146
|
+
detail: `Could not read budget notifications (${all.errorCode || 'unknown error'}), so it is unknown whether ${coveringBudgets.join(', ')} alerts anyone.`,
|
|
147
|
+
remediation: 'Grant budgets:DescribeBudgetNotificationsForAccount and budgets:DescribeSubscribersForNotification, then re-run.',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const perBudget = all.json?.BudgetNotificationsForAccount ?? [];
|
|
151
|
+
const silent = [];
|
|
152
|
+
const unsubscribed = [];
|
|
153
|
+
let alerting = 0;
|
|
154
|
+
for (const name of coveringBudgets) {
|
|
155
|
+
const entry = perBudget.find((e) => e.BudgetName === name);
|
|
156
|
+
const notifications = entry?.Notifications ?? [];
|
|
157
|
+
if (notifications.length === 0) {
|
|
158
|
+
silent.push(name);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
let subscribers = 0;
|
|
162
|
+
for (const n of notifications) {
|
|
163
|
+
const subs = aws([
|
|
164
|
+
'budgets', 'describe-subscribers-for-notification',
|
|
165
|
+
'--account-id', accountId, '--budget-name', name,
|
|
166
|
+
'--notification', JSON.stringify(n),
|
|
167
|
+
], target);
|
|
168
|
+
if (subs.ok)
|
|
169
|
+
subscribers += (subs.json?.Subscribers ?? []).length;
|
|
170
|
+
}
|
|
171
|
+
if (subscribers === 0)
|
|
172
|
+
unsubscribed.push(name);
|
|
173
|
+
else
|
|
174
|
+
alerting++;
|
|
175
|
+
}
|
|
176
|
+
const broken = [...silent, ...unsubscribed];
|
|
177
|
+
return {
|
|
178
|
+
id, category: 'budget', title,
|
|
179
|
+
status: broken.length ? 'FAIL' : 'PASS', severity: 'HIGH',
|
|
180
|
+
detail: broken.length
|
|
181
|
+
? [
|
|
182
|
+
silent.length ? `${silent.join(', ')} has no notification thresholds at all` : '',
|
|
183
|
+
unsubscribed.length ? `${unsubscribed.join(', ')} has thresholds but no subscribers` : '',
|
|
184
|
+
].filter(Boolean).join('; ')
|
|
185
|
+
+ '. A budget with nothing to notify records the overspend without telling anyone, which reads as protection while providing none.'
|
|
186
|
+
: `All ${alerting} covering budget(s) have at least one threshold with a subscriber.`,
|
|
187
|
+
remediation: broken.length
|
|
188
|
+
? 'Add notification thresholds (50/80/100%) with an SNS topic or email subscriber. Prefer SNS — it can fan out to a rota rather than one mailbox.'
|
|
189
|
+
: undefined,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* One title constant per check, because this check is pushed from three
|
|
194
|
+
* branches (CE disabled, CE unreadable, CE readable). Inlining the string in
|
|
195
|
+
* each made them drift the moment the check gained its subscription
|
|
196
|
+
* requirement: the same `anomaly-monitor` id reported two different titles
|
|
197
|
+
* depending on account state, which is unstable in --json output.
|
|
198
|
+
*/
|
|
199
|
+
const ANOMALY_TITLE = 'Cost anomaly monitor covering Bedrock, with an alert subscription';
|
|
200
|
+
export function auditDetection(target, region) {
|
|
201
|
+
const out = [];
|
|
202
|
+
// Cost Explorer is an account-level opt-in that stays OFF until someone
|
|
203
|
+
// enables it -- measured error: AccessDeniedException "User not enabled for
|
|
204
|
+
// cost explorer access". Every CE-backed check must be gated on this, or the
|
|
205
|
+
// audit reports "no anomaly monitor configured" when the truth is that it
|
|
206
|
+
// could not look.
|
|
207
|
+
const ce = aws(['ce', 'get-anomaly-monitors'], target);
|
|
208
|
+
const ceDisabled = !ce.ok && /not enabled for cost explorer/i.test(ce.raw);
|
|
209
|
+
out.push({
|
|
210
|
+
id: 'cost-explorer-enabled', category: 'detection', title: 'Cost Explorer enabled',
|
|
211
|
+
status: ceDisabled ? 'FAIL' : ce.ok ? 'PASS' : 'INDETERMINATE',
|
|
212
|
+
severity: 'HIGH',
|
|
213
|
+
detail: ceDisabled
|
|
214
|
+
? 'Cost Explorer is not enabled on this account, so cost anomaly detection is impossible and spend cannot be attributed to a service or model.'
|
|
215
|
+
: ce.ok
|
|
216
|
+
? 'Cost Explorer is enabled.'
|
|
217
|
+
: `Could not confirm Cost Explorer access (${ce.errorCode || 'unknown error'}).`,
|
|
218
|
+
remediation: ceDisabled ? 'Enable Cost Explorer in Billing settings (management account for an org). It backfills up to 12 months and can take 24h to become queryable.' : undefined,
|
|
219
|
+
});
|
|
220
|
+
if (ceDisabled || !ce.ok) {
|
|
221
|
+
out.push({
|
|
222
|
+
id: 'anomaly-monitor', category: 'detection', title: ANOMALY_TITLE,
|
|
223
|
+
status: 'INDETERMINATE', severity: 'MEDIUM',
|
|
224
|
+
detail: ceDisabled
|
|
225
|
+
? 'Cannot be checked while Cost Explorer is disabled.'
|
|
226
|
+
: `Could not list anomaly monitors (${ce.errorCode || 'unknown error'}).`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const monitors = ce.json?.AnomalyMonitors ?? [];
|
|
231
|
+
const serviceWide = monitors.filter((m) => m.MonitorDimension === 'SERVICE');
|
|
232
|
+
const bedrockScoped = monitors.filter((m) => JSON.stringify(m.MonitorSpecification ?? {}).toLowerCase().includes('bedrock'));
|
|
233
|
+
const covering = [...serviceWide, ...bedrockScoped];
|
|
234
|
+
// A monitor with no subscription is the same failure mode as a budget with
|
|
235
|
+
// no subscriber: it detects the anomaly and tells nobody. Monitors and
|
|
236
|
+
// subscriptions are separate resources, and get-anomaly-monitors reports
|
|
237
|
+
// nothing about whether one is attached, so this needs a second call.
|
|
238
|
+
//
|
|
239
|
+
// Worth knowing: creating a first budget auto-enables Cost Explorer, and AWS
|
|
240
|
+
// then auto-creates a "Default-Services-Monitor" (SERVICE dimension) WITH a
|
|
241
|
+
// subscription -- observed live. So this check often passes as a side effect
|
|
242
|
+
// of setting up budgets rather than deliberately.
|
|
243
|
+
const subs = aws(['ce', 'get-anomaly-subscriptions'], target);
|
|
244
|
+
const coveringArns = new Set(covering.map((m) => m.MonitorArn));
|
|
245
|
+
const subscribed = subs.ok
|
|
246
|
+
? (subs.json?.AnomalySubscriptions ?? []).some((s) => (s.MonitorArnList ?? []).some((arn) => coveringArns.has(arn)))
|
|
247
|
+
: null;
|
|
248
|
+
const status = covering.length === 0 ? 'FAIL' : subscribed === null ? 'INDETERMINATE' : subscribed ? 'PASS' : 'FAIL';
|
|
249
|
+
out.push({
|
|
250
|
+
id: 'anomaly-monitor', category: 'detection', title: ANOMALY_TITLE,
|
|
251
|
+
status, severity: 'MEDIUM',
|
|
252
|
+
detail: covering.length === 0
|
|
253
|
+
? (monitors.length === 0
|
|
254
|
+
? 'No cost anomaly monitors. A budget only fires once a threshold is crossed; anomaly detection catches spend that is abnormal for this account, which is what a stolen key looks like on day one.'
|
|
255
|
+
: `${monitors.length} monitor(s) exist but none covers Bedrock.`)
|
|
256
|
+
: subscribed === null
|
|
257
|
+
? `${covering.length} covering monitor(s), but the subscription list could not be read (${subs.errorCode || 'unknown error'}), so it is unknown whether anomalies reach anyone.`
|
|
258
|
+
: subscribed
|
|
259
|
+
? `${covering.length} covering monitor(s) (${serviceWide.length} SERVICE-dimension, ${bedrockScoped.length} Bedrock-scoped), with an alert subscription attached.`
|
|
260
|
+
: `${covering.length} covering monitor(s) exist but NO subscription references them, so detected anomalies notify nobody — the monitor records them and stops there.`,
|
|
261
|
+
remediation: status === 'PASS' ? undefined
|
|
262
|
+
: covering.length === 0
|
|
263
|
+
? 'Create a SERVICE-dimension monitor — it covers every service including Marketplace — then attach a subscription with a threshold low enough to fire before the monthly budget would.'
|
|
264
|
+
: 'Attach an anomaly subscription to the monitor. Without one the monitor is detection with no delivery.',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// CloudWatch is the only fast signal. Cost data lags 12-24h, so a cost-only
|
|
268
|
+
// posture lets a stolen key run for up to a day before anything fires.
|
|
269
|
+
const alarms = aws(['cloudwatch', 'describe-alarms', '--region', region], target);
|
|
270
|
+
if (!alarms.ok) {
|
|
271
|
+
out.push({
|
|
272
|
+
id: 'bedrock-invocation-alarm', category: 'detection', title: 'CloudWatch alarm on Bedrock invocation volume',
|
|
273
|
+
status: 'INDETERMINATE', severity: 'MEDIUM',
|
|
274
|
+
detail: `Could not list alarms in ${region} (${alarms.errorCode || 'unknown error'}).`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
const bedrockAlarms = (alarms.json?.MetricAlarms ?? []).filter((a) => a.Namespace === 'AWS/Bedrock');
|
|
279
|
+
out.push({
|
|
280
|
+
id: 'bedrock-invocation-alarm', category: 'detection', title: 'CloudWatch alarm on Bedrock invocation volume',
|
|
281
|
+
status: bedrockAlarms.length ? 'PASS' : 'FAIL', severity: 'MEDIUM',
|
|
282
|
+
detail: bedrockAlarms.length
|
|
283
|
+
? `${bedrockAlarms.length} alarm(s) on AWS/Bedrock metrics: ${bedrockAlarms.map((a) => a.AlarmName).slice(0, 5).join(', ')}.`
|
|
284
|
+
: `No alarms on AWS/Bedrock metrics in ${region}. Cost data lags 12–24h, so budgets and anomaly detection cannot catch a stolen key inside the first day. Token-count metrics are near real-time and are the only fast signal available.`,
|
|
285
|
+
remediation: bedrockAlarms.length ? undefined : 'Alarm on AWS/Bedrock Invocations and InputTokenCount with a threshold above normal peak; it fires in minutes rather than hours.',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
export function auditCommitments(target, region) {
|
|
291
|
+
const pt = aws(['bedrock', 'list-provisioned-model-throughputs', '--region', region], target);
|
|
292
|
+
if (!pt.ok) {
|
|
293
|
+
return {
|
|
294
|
+
id: 'provisioned-throughput', category: 'commitment', title: 'Provisioned Throughput commitments',
|
|
295
|
+
status: 'INDETERMINATE', severity: 'INFO',
|
|
296
|
+
detail: `Could not list provisioned throughput in ${region} (${pt.errorCode || 'unknown error'}).`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const pts = pt.json?.provisionedModelSummaries ?? [];
|
|
300
|
+
return {
|
|
301
|
+
id: 'provisioned-throughput', category: 'commitment', title: 'Provisioned Throughput commitments',
|
|
302
|
+
status: pts.length ? 'FAIL' : 'PASS',
|
|
303
|
+
severity: pts.length ? 'MEDIUM' : 'INFO',
|
|
304
|
+
detail: pts.length
|
|
305
|
+
? `${pts.length} commitment(s) in ${region}: ${pts.map((p) => p.provisionedModelName || p.modelArn).slice(0, 5).join(', ')}. These bill monthly whether or not they are used.`
|
|
306
|
+
: `No Provisioned Throughput commitments in ${region}. NOTE: region-scoped — a commitment in another region will not appear here.`,
|
|
307
|
+
remediation: pts.length ? 'Confirm each commitment is intentional and still in use; an abandoned one bills indefinitely.' : undefined,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
export function auditForensics(target, region) {
|
|
311
|
+
// Returns an EMPTY body when logging is off -- same trap as describe-budgets.
|
|
312
|
+
const log = aws(['bedrock', 'get-model-invocation-logging-configuration', '--region', region], target);
|
|
313
|
+
if (!log.ok) {
|
|
314
|
+
return {
|
|
315
|
+
id: 'model-invocation-logging', category: 'forensics', title: 'Bedrock model invocation logging',
|
|
316
|
+
status: 'INDETERMINATE', severity: 'MEDIUM',
|
|
317
|
+
detail: `Could not read the logging configuration in ${region} (${log.errorCode || 'unknown error'}).`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const cfg = log.json?.loggingConfig;
|
|
321
|
+
const configured = Boolean(cfg && (cfg.cloudWatchConfig || cfg.s3Config));
|
|
322
|
+
return {
|
|
323
|
+
id: 'model-invocation-logging', category: 'forensics', title: 'Bedrock model invocation logging',
|
|
324
|
+
status: configured ? 'PASS' : 'FAIL', severity: 'MEDIUM',
|
|
325
|
+
detail: configured
|
|
326
|
+
? `Enabled in ${region} (${[cfg.cloudWatchConfig && 'CloudWatch', cfg.s3Config && 'S3'].filter(Boolean).join(' + ')}).`
|
|
327
|
+
: `Off in ${region}. CloudTrail records that InvokeModel was called but not the model, prompt or token counts, so during an incident you can see spend rose without being able to attribute it to a caller or workload.`,
|
|
328
|
+
remediation: configured ? undefined : 'Enable model invocation logging to CloudWatch Logs or S3. Prompts and completions are captured, so treat the destination as sensitive and scope its access accordingly.',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
//# sourceMappingURL=audit-bedrock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-bedrock.js","sourceRoot":"","sources":["../../../src/utils/audit-bedrock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AwsTarget } from './aws.js';
|
|
2
|
+
import { Finding } from './audit.js';
|
|
3
|
+
export declare function auditRoot(target?: AwsTarget): Finding[];
|
|
4
|
+
export declare function auditPasswordPolicy(target?: AwsTarget): Finding;
|
|
5
|
+
export declare function auditUsers(target: AwsTarget | undefined, maxKeyAge: number, unusedDays: number): Finding[];
|
|
6
|
+
export declare function auditUserPolicies(target?: AwsTarget): Finding;
|
|
7
|
+
//# sourceMappingURL=audit-iam.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-iam.d.ts","sourceRoot":"","sources":["../../../src/utils/audit-iam.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,SAAS,EAAa,MAAM,UAAU,CAAC;AACrD,OAAO,EAAE,OAAO,EAAY,MAAM,YAAY,CAAC;AAwE/C,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,EAAE,CA6CvD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAmC/D;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,EAAE,CA2E1G;AAED,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAgC7D"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { aws, daysSince } from './aws.js';
|
|
2
|
+
/**
|
|
3
|
+
* GetCredentialReport fails with ReportNotPresent until GenerateCredentialReport
|
|
4
|
+
* has been called -- measured, not assumed. Generation is asynchronous, so this
|
|
5
|
+
* kicks it off and re-reads a bounded number of times.
|
|
6
|
+
*
|
|
7
|
+
* This is the only non-read AWS call in the command. It produces a report
|
|
8
|
+
* artifact and modifies no resource.
|
|
9
|
+
*/
|
|
10
|
+
function credentialReport(target) {
|
|
11
|
+
let got = aws(['iam', 'get-credential-report'], target);
|
|
12
|
+
if (!got.ok && got.errorCode === 'ReportNotPresent') {
|
|
13
|
+
const gen = aws(['iam', 'generate-credential-report'], target);
|
|
14
|
+
if (!gen.ok)
|
|
15
|
+
return { error: gen.errorCode || 'generate-credential-report failed' };
|
|
16
|
+
for (let i = 0; i < 10 && !got.ok; i++) {
|
|
17
|
+
got = aws(['iam', 'get-credential-report'], target);
|
|
18
|
+
if (!got.ok && got.errorCode !== 'ReportNotPresent')
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!got.ok)
|
|
23
|
+
return { error: got.errorCode || 'get-credential-report failed' };
|
|
24
|
+
const b64 = got.json?.Content;
|
|
25
|
+
if (!b64)
|
|
26
|
+
return { error: 'credential report had no Content' };
|
|
27
|
+
const csv = Buffer.from(b64, 'base64').toString('utf8');
|
|
28
|
+
const lines = csv.trim().split('\n');
|
|
29
|
+
const header = lines[0].split(',');
|
|
30
|
+
const col = (name) => header.indexOf(name);
|
|
31
|
+
const rows = [];
|
|
32
|
+
for (const line of lines.slice(1)) {
|
|
33
|
+
const c = line.split(',');
|
|
34
|
+
const at = (name) => (col(name) >= 0 ? c[col(name)] : '');
|
|
35
|
+
rows.push({
|
|
36
|
+
user: at('user'),
|
|
37
|
+
passwordEnabled: at('password_enabled') === 'true',
|
|
38
|
+
mfaActive: at('mfa_active') === 'true',
|
|
39
|
+
passwordLastUsed: at('password_last_used'),
|
|
40
|
+
keys: [1, 2].map((n) => ({
|
|
41
|
+
n,
|
|
42
|
+
active: at(`access_key_${n}_active`) === 'true',
|
|
43
|
+
lastRotated: at(`access_key_${n}_last_rotated`),
|
|
44
|
+
lastUsed: at(`access_key_${n}_last_used_date`),
|
|
45
|
+
})),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return rows;
|
|
49
|
+
}
|
|
50
|
+
export function auditRoot(target) {
|
|
51
|
+
const out = [];
|
|
52
|
+
// GetAccountSummary answers the two most severe questions in one call and
|
|
53
|
+
// needs no credential report. AccountMFAEnabled and AccountAccessKeysPresent
|
|
54
|
+
// both describe the ROOT user specifically.
|
|
55
|
+
const summary = aws(['iam', 'get-account-summary'], target);
|
|
56
|
+
const map = summary.ok ? (summary.json?.SummaryMap ?? null) : null;
|
|
57
|
+
if (!map) {
|
|
58
|
+
out.push({
|
|
59
|
+
id: 'root-mfa', category: 'root', title: 'Root account MFA',
|
|
60
|
+
status: 'INDETERMINATE', severity: 'CRITICAL',
|
|
61
|
+
detail: `Could not read the account summary (${summary.errorCode || 'unknown error'}).`,
|
|
62
|
+
remediation: 'Grant iam:GetAccountSummary and re-run.',
|
|
63
|
+
});
|
|
64
|
+
out.push({
|
|
65
|
+
id: 'root-access-keys', category: 'root', title: 'Root access keys',
|
|
66
|
+
status: 'INDETERMINATE', severity: 'CRITICAL',
|
|
67
|
+
detail: `Could not read the account summary (${summary.errorCode || 'unknown error'}).`,
|
|
68
|
+
});
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
const rootMfa = map.AccountMFAEnabled === 1;
|
|
72
|
+
out.push({
|
|
73
|
+
id: 'root-mfa', category: 'root', title: 'Root account MFA',
|
|
74
|
+
status: rootMfa ? 'PASS' : 'FAIL', severity: 'CRITICAL',
|
|
75
|
+
detail: rootMfa
|
|
76
|
+
? 'Root user has an MFA device.'
|
|
77
|
+
: 'Root user has NO MFA device. Root cannot be constrained by SCPs or permission boundaries, so a compromised root is unbounded.',
|
|
78
|
+
remediation: 'Attach a FIDO2 security key to root — phishing resistant, unlike TOTP. In Amazon-internal Isengard accounts root is managed for you and this may not be actionable; confirm before escalating.',
|
|
79
|
+
});
|
|
80
|
+
const rootKeys = map.AccountAccessKeysPresent === 1;
|
|
81
|
+
out.push({
|
|
82
|
+
id: 'root-access-keys', category: 'root', title: 'Root access keys',
|
|
83
|
+
status: rootKeys ? 'FAIL' : 'PASS', severity: 'CRITICAL',
|
|
84
|
+
detail: rootKeys
|
|
85
|
+
? 'Root has active access keys — unconstrainable long-lived credentials, the single worst credential to leak.'
|
|
86
|
+
: 'No root access keys.',
|
|
87
|
+
remediation: 'Delete root access keys. Nothing legitimate requires them.',
|
|
88
|
+
});
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
export function auditPasswordPolicy(target) {
|
|
92
|
+
const pol = aws(['iam', 'get-account-password-policy'], target);
|
|
93
|
+
// NoSuchEntity is the API saying "no policy set", which means AWS defaults
|
|
94
|
+
// apply. That is a finding, not an error -- treating it as an error hides it.
|
|
95
|
+
if (!pol.ok && pol.errorCode === 'NoSuchEntity') {
|
|
96
|
+
return {
|
|
97
|
+
id: 'password-policy', category: 'policy', title: 'Account password policy',
|
|
98
|
+
status: 'FAIL', severity: 'MEDIUM',
|
|
99
|
+
detail: 'No password policy is set, so only AWS defaults apply (minimum length 8, no complexity or reuse rules).',
|
|
100
|
+
remediation: 'Set MinimumPasswordLength >= 14 with complexity and reuse prevention — or remove console passwords entirely in favour of Identity Center.',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (!pol.ok) {
|
|
104
|
+
return {
|
|
105
|
+
id: 'password-policy', category: 'policy', title: 'Account password policy',
|
|
106
|
+
status: 'INDETERMINATE', severity: 'MEDIUM',
|
|
107
|
+
detail: `Could not read the password policy (${pol.errorCode || 'unknown error'}).`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const p = pol.json?.PasswordPolicy ?? {};
|
|
111
|
+
const weak = [];
|
|
112
|
+
if ((p.MinimumPasswordLength ?? 0) < 14)
|
|
113
|
+
weak.push(`MinimumPasswordLength=${p.MinimumPasswordLength ?? 'unset'} (< 14)`);
|
|
114
|
+
if (!p.RequireSymbols)
|
|
115
|
+
weak.push('RequireSymbols=false');
|
|
116
|
+
if (!p.RequireNumbers)
|
|
117
|
+
weak.push('RequireNumbers=false');
|
|
118
|
+
if (!p.RequireUppercaseCharacters || !p.RequireLowercaseCharacters)
|
|
119
|
+
weak.push('mixed case not required');
|
|
120
|
+
if (!p.PasswordReusePrevention)
|
|
121
|
+
weak.push('PasswordReusePrevention unset');
|
|
122
|
+
return {
|
|
123
|
+
id: 'password-policy', category: 'policy', title: 'Account password policy',
|
|
124
|
+
status: weak.length ? 'FAIL' : 'PASS', severity: 'MEDIUM',
|
|
125
|
+
detail: weak.length ? `Weaker than baseline: ${weak.join(', ')}.` : 'Policy meets the baseline.',
|
|
126
|
+
remediation: weak.length ? 'Raise minimum length to 14+, require complexity, enable reuse prevention.' : undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export function auditUsers(target, maxKeyAge, unusedDays) {
|
|
130
|
+
const out = [];
|
|
131
|
+
const report = credentialReport(target);
|
|
132
|
+
if ('error' in report) {
|
|
133
|
+
for (const [id, title, sev] of [
|
|
134
|
+
['access-key-age', 'Long-lived access keys', 'HIGH'],
|
|
135
|
+
['console-user-no-mfa', 'Console users without MFA', 'HIGH'],
|
|
136
|
+
['unused-credentials', 'Unused credentials', 'MEDIUM'],
|
|
137
|
+
]) {
|
|
138
|
+
out.push({
|
|
139
|
+
id, category: 'users', title,
|
|
140
|
+
status: 'INDETERMINATE', severity: sev,
|
|
141
|
+
detail: `Credential report unavailable (${report.error}).`,
|
|
142
|
+
remediation: 'Grant iam:GenerateCredentialReport and iam:GetCredentialReport, then re-run.',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
// <root_account> is covered by the summary checks and reported with a
|
|
148
|
+
// different shape, so exclude it here rather than double-reporting.
|
|
149
|
+
const users = report.filter((r) => r.user !== '<root_account>');
|
|
150
|
+
const stale = [];
|
|
151
|
+
for (const u of users) {
|
|
152
|
+
for (const k of u.keys) {
|
|
153
|
+
if (!k.active)
|
|
154
|
+
continue;
|
|
155
|
+
const age = daysSince(k.lastRotated);
|
|
156
|
+
if (age !== null && age > maxKeyAge)
|
|
157
|
+
stale.push(`${u.user} key ${k.n} (${age}d)`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
out.push({
|
|
161
|
+
id: 'access-key-age', category: 'users', title: 'Long-lived access keys',
|
|
162
|
+
status: stale.length ? 'FAIL' : 'PASS', severity: 'HIGH',
|
|
163
|
+
detail: stale.length
|
|
164
|
+
? `${stale.length} active key(s) older than ${maxKeyAge}d: ${stale.slice(0, 8).join(', ')}${stale.length > 8 ? ', …' : ''}. Key age is the strongest single predictor of credential-theft exposure — an old key has had more opportunities to leak and is rarely missed when it does.`
|
|
165
|
+
: `No active access key is older than ${maxKeyAge}d.`,
|
|
166
|
+
remediation: stale.length ? 'Rotate, or delete the key and move the workload onto an IAM role. A role issues short-lived credentials that cannot be exfiltrated as a static string.' : undefined,
|
|
167
|
+
});
|
|
168
|
+
const noMfa = users.filter((u) => u.passwordEnabled && !u.mfaActive).map((u) => u.user);
|
|
169
|
+
out.push({
|
|
170
|
+
id: 'console-user-no-mfa', category: 'users', title: 'Console users without MFA',
|
|
171
|
+
status: noMfa.length ? 'FAIL' : 'PASS', severity: 'HIGH',
|
|
172
|
+
detail: noMfa.length
|
|
173
|
+
? `${noMfa.length} user(s) have console passwords and no MFA: ${noMfa.slice(0, 8).join(', ')}${noMfa.length > 8 ? ', …' : ''}.`
|
|
174
|
+
: 'Every console-enabled user has MFA.',
|
|
175
|
+
remediation: noMfa.length ? 'Enforce MFA with a deny policy conditioned on aws:MultiFactorAuthPresent rather than relying on voluntary enrolment.' : undefined,
|
|
176
|
+
});
|
|
177
|
+
const unused = [];
|
|
178
|
+
for (const u of users) {
|
|
179
|
+
if (u.passwordEnabled) {
|
|
180
|
+
const d = daysSince(u.passwordLastUsed);
|
|
181
|
+
if (d !== null && d > unusedDays)
|
|
182
|
+
unused.push(`${u.user} password (${d}d)`);
|
|
183
|
+
}
|
|
184
|
+
for (const k of u.keys) {
|
|
185
|
+
if (!k.active)
|
|
186
|
+
continue;
|
|
187
|
+
// 'N/A' means active but never used; fall back to key age so a
|
|
188
|
+
// never-used key is not silently skipped.
|
|
189
|
+
const d = daysSince(k.lastUsed) ?? daysSince(k.lastRotated);
|
|
190
|
+
if (d !== null && d > unusedDays)
|
|
191
|
+
unused.push(`${u.user} key ${k.n} (${d}d)`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
out.push({
|
|
195
|
+
id: 'unused-credentials', category: 'users', title: 'Unused credentials',
|
|
196
|
+
status: unused.length ? 'FAIL' : 'PASS', severity: 'MEDIUM',
|
|
197
|
+
detail: unused.length
|
|
198
|
+
? `${unused.length} credential(s) idle for over ${unusedDays}d: ${unused.slice(0, 8).join(', ')}${unused.length > 8 ? ', …' : ''}. An idle credential is pure attack surface — its first malicious use looks identical to its first legitimate use.`
|
|
199
|
+
: `No credential has been idle longer than ${unusedDays}d.`,
|
|
200
|
+
remediation: unused.length ? 'Delete them. Idle credentials are the ones whose misuse goes unnoticed longest.' : undefined,
|
|
201
|
+
});
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
export function auditUserPolicies(target) {
|
|
205
|
+
const list = aws(['iam', 'list-users', '--query', 'Users[].UserName'], target);
|
|
206
|
+
if (!list.ok) {
|
|
207
|
+
return {
|
|
208
|
+
id: 'user-admin-policy', category: 'users', title: 'AdministratorAccess on IAM users',
|
|
209
|
+
status: 'INDETERMINATE', severity: 'HIGH',
|
|
210
|
+
detail: `Could not list users (${list.errorCode || 'unknown error'}).`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const names = Array.isArray(list.json) ? list.json : [];
|
|
214
|
+
const admins = [];
|
|
215
|
+
let unchecked = 0;
|
|
216
|
+
for (const name of names) {
|
|
217
|
+
const att = aws(['iam', 'list-attached-user-policies', '--user-name', name], target);
|
|
218
|
+
if (!att.ok) {
|
|
219
|
+
unchecked++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const policies = att.json?.AttachedPolicies ?? [];
|
|
223
|
+
if (policies.some((p) => p.PolicyName === 'AdministratorAccess'))
|
|
224
|
+
admins.push(name);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
id: 'user-admin-policy', category: 'users', title: 'AdministratorAccess on IAM users',
|
|
228
|
+
// A partial read cannot clear the check: unchecked users may be the admins.
|
|
229
|
+
status: admins.length ? 'FAIL' : unchecked > 0 ? 'INDETERMINATE' : 'PASS',
|
|
230
|
+
severity: 'HIGH',
|
|
231
|
+
detail: admins.length
|
|
232
|
+
? `AdministratorAccess is attached directly to: ${admins.join(', ')}. Any access key belonging to these users is an unbounded credential.`
|
|
233
|
+
: unchecked > 0
|
|
234
|
+
? `Checked ${names.length - unchecked}/${names.length} users; ${unchecked} could not be read, so this result is incomplete.`
|
|
235
|
+
: `None of ${names.length} user(s) has AdministratorAccess attached directly.`,
|
|
236
|
+
remediation: admins.length ? 'Move admin access behind an assumable role gated on MFA, so no static key carries it.' : undefined,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=audit-iam.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-iam.js","sourceRoot":"","sources":["../../../src/utils/audit-iam.ts"],"names":[],"mappings":"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"}
|