infrawise 0.12.6 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -151,7 +151,7 @@ Add to your editor's MCP config:
|
|
|
151
151
|
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
152
152
|
| `get_infra_overview` | Complete snapshot — all services, counts, high-severity findings, and a `configured` flag |
|
|
153
153
|
| `get_graph_summary` | Full infrastructure graph — all nodes, edges, and findings |
|
|
154
|
-
| `analyze_function` | Issues in a specific function — scans, missing indexes, N+1, trigger event shapes
|
|
154
|
+
| `analyze_function` | Issues in a specific function — scans, missing indexes, N+1, trigger event shapes, missing IAM permissions |
|
|
155
155
|
| `suggest_gsi` | Exact GSI config for a DynamoDB table + attribute |
|
|
156
156
|
| `postgres_index_suggestions` | Exact `CREATE INDEX` SQL for your actual table |
|
|
157
157
|
| `suggest_mongo_index` | Exact `createIndex` command for a MongoDB collection + field |
|
|
@@ -161,7 +161,7 @@ Add to your editor's MCP config:
|
|
|
161
161
|
| `get_topic_details` | SNS topics — subscription counts, protocols, and filter policies (required message attributes per subscription) |
|
|
162
162
|
| `get_secrets_overview` | Secrets Manager — names and rotation status (values never included) |
|
|
163
163
|
| `get_parameter_overview` | SSM Parameter Store — names, types, tiers (values never included) |
|
|
164
|
-
| `get_lambda_overview` | Lambda functions — runtime, memory, timeout, triggers (SQS/SNS/DynamoDB/Kinesis/MSK/EventBridge/S3), env var key names |
|
|
164
|
+
| `get_lambda_overview` | Lambda functions — runtime, memory, timeout, execution role ARN, triggers (SQS/SNS/DynamoDB/Kinesis/MSK/EventBridge/S3), env var key names |
|
|
165
165
|
| `get_eventbridge_details` | EventBridge rules — name, state, schedule/event pattern, target functions |
|
|
166
166
|
| `get_s3_overview` | S3 buckets — versioning, encryption, public access, event notifications |
|
|
167
167
|
| `get_log_errors` | CloudWatch error patterns and counts (no raw log messages) |
|
|
@@ -7,6 +7,7 @@ import { SecretsManagerClient, ListSecretsCommand } from '@aws-sdk/client-secret
|
|
|
7
7
|
import { LambdaClient, ListFunctionsCommand, ListEventSourceMappingsCommand, } from '@aws-sdk/client-lambda';
|
|
8
8
|
import { EventBridgeClient, ListRulesCommand, ListTargetsByRuleCommand, } from '@aws-sdk/client-eventbridge';
|
|
9
9
|
import { RDSClient, DescribeDBInstancesCommand } from '@aws-sdk/client-rds';
|
|
10
|
+
import { IAMClient, ListAttachedRolePoliciesCommand, GetPolicyCommand, GetPolicyVersionCommand, ListRolePoliciesCommand, GetRolePolicyCommand, } from '@aws-sdk/client-iam';
|
|
10
11
|
import { fromIni } from '@aws-sdk/credential-providers';
|
|
11
12
|
import { logger } from '../../core/index.js';
|
|
12
13
|
function clientConfig(cfg) {
|
|
@@ -209,6 +210,77 @@ export async function extractSecretsMetadata(cfg = {}) {
|
|
|
209
210
|
export async function validateSecretsAccess(cfg = {}) {
|
|
210
211
|
await new SecretsManagerClient(clientConfig(cfg)).send(new ListSecretsCommand({ MaxResults: 1 }));
|
|
211
212
|
}
|
|
213
|
+
function servicesFromDoc(doc) {
|
|
214
|
+
const out = new Set();
|
|
215
|
+
for (const stmt of doc.Statement ?? []) {
|
|
216
|
+
if (stmt.Effect !== 'Allow')
|
|
217
|
+
continue;
|
|
218
|
+
const actions = Array.isArray(stmt.Action) ? stmt.Action : stmt.Action ? [stmt.Action] : [];
|
|
219
|
+
for (const a of actions) {
|
|
220
|
+
if (a === '*') {
|
|
221
|
+
out.add('*');
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const prefix = a.split(':')[0].toLowerCase();
|
|
225
|
+
if (prefix)
|
|
226
|
+
out.add(prefix);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return [...out];
|
|
230
|
+
}
|
|
231
|
+
async function extractAllowedServices(roleArn, cfg) {
|
|
232
|
+
const client = new IAMClient(clientConfig(cfg));
|
|
233
|
+
const roleName = roleArn.split('/').pop();
|
|
234
|
+
const services = new Set();
|
|
235
|
+
try {
|
|
236
|
+
let marker;
|
|
237
|
+
do {
|
|
238
|
+
const res = await client.send(new ListAttachedRolePoliciesCommand({ RoleName: roleName, Marker: marker }));
|
|
239
|
+
for (const policy of res.AttachedPolicies ?? []) {
|
|
240
|
+
try {
|
|
241
|
+
const meta = await client.send(new GetPolicyCommand({ PolicyArn: policy.PolicyArn }));
|
|
242
|
+
const versionId = meta.Policy?.DefaultVersionId;
|
|
243
|
+
if (!versionId)
|
|
244
|
+
continue;
|
|
245
|
+
const ver = await client.send(new GetPolicyVersionCommand({ PolicyArn: policy.PolicyArn, VersionId: versionId }));
|
|
246
|
+
const doc = ver.PolicyVersion?.Document;
|
|
247
|
+
if (doc) {
|
|
248
|
+
const parsed = JSON.parse(decodeURIComponent(doc));
|
|
249
|
+
for (const s of servicesFromDoc(parsed))
|
|
250
|
+
services.add(s);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
/* skip unparseable policy */
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
marker = res.Marker;
|
|
258
|
+
} while (marker);
|
|
259
|
+
let iMarker;
|
|
260
|
+
do {
|
|
261
|
+
const res = await client.send(new ListRolePoliciesCommand({ RoleName: roleName, Marker: iMarker }));
|
|
262
|
+
for (const name of res.PolicyNames ?? []) {
|
|
263
|
+
try {
|
|
264
|
+
const inline = await client.send(new GetRolePolicyCommand({ RoleName: roleName, PolicyName: name }));
|
|
265
|
+
if (inline.PolicyDocument) {
|
|
266
|
+
const parsed = JSON.parse(decodeURIComponent(inline.PolicyDocument));
|
|
267
|
+
for (const s of servicesFromDoc(parsed))
|
|
268
|
+
services.add(s);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
/* skip */
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
iMarker = res.Marker;
|
|
276
|
+
} while (iMarker);
|
|
277
|
+
return [...services];
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
logger.debug(`IAM fetch skipped for ${roleName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
212
284
|
// ─── Lambda ───────────────────────────────────────────────────────────────────
|
|
213
285
|
const EVENT_SHAPES = {
|
|
214
286
|
sqs: 'event.Records[0].body',
|
|
@@ -319,6 +391,7 @@ export async function extractLambdaMetadata(cfg = {}, includeFunctions) {
|
|
|
319
391
|
envVarKeys: Object.keys(fn.Environment?.Variables ?? {}),
|
|
320
392
|
layers: (fn.Layers ?? []).map((l) => l.Arn ?? '').filter(Boolean),
|
|
321
393
|
triggers: [],
|
|
394
|
+
roleArn: fn.Role,
|
|
322
395
|
});
|
|
323
396
|
}
|
|
324
397
|
marker = res.NextMarker;
|
|
@@ -328,6 +401,16 @@ export async function extractLambdaMetadata(cfg = {}, includeFunctions) {
|
|
|
328
401
|
for (const fn of functions) {
|
|
329
402
|
fn.triggers = triggerMap.get(fn.arn) ?? [];
|
|
330
403
|
}
|
|
404
|
+
// Batch IAM policy fetch per unique role ARN
|
|
405
|
+
const uniqueRoles = [...new Set(functions.map((f) => f.roleArn).filter(Boolean))];
|
|
406
|
+
const roleServices = new Map();
|
|
407
|
+
await Promise.all(uniqueRoles.map(async (arn) => {
|
|
408
|
+
roleServices.set(arn, await extractAllowedServices(arn, cfg));
|
|
409
|
+
}));
|
|
410
|
+
for (const fn of functions) {
|
|
411
|
+
if (fn.roleArn)
|
|
412
|
+
fn.allowedServices = roleServices.get(fn.roleArn);
|
|
413
|
+
}
|
|
331
414
|
}
|
|
332
415
|
catch (err) {
|
|
333
416
|
logger.warn(`Lambda list failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -284,3 +284,67 @@ export class S3UnencryptedAnalyzer {
|
|
|
284
284
|
return findings;
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
+
// ─── IAM ─────────────────────────────────────────────────────────────────────
|
|
288
|
+
const MINIMAL_ACTIONS = {
|
|
289
|
+
dynamodb: 'dynamodb:GetItem, dynamodb:PutItem, dynamodb:Query, dynamodb:UpdateItem, dynamodb:DeleteItem',
|
|
290
|
+
secretsmanager: 'secretsmanager:GetSecretValue',
|
|
291
|
+
ssm: 'ssm:GetParameter, ssm:GetParameters, ssm:GetParametersByPath',
|
|
292
|
+
sqs: 'sqs:SendMessage, sqs:ReceiveMessage, sqs:DeleteMessage, sqs:GetQueueAttributes',
|
|
293
|
+
sns: 'sns:Publish',
|
|
294
|
+
s3: 's3:GetObject, s3:PutObject, s3:DeleteObject',
|
|
295
|
+
};
|
|
296
|
+
export class LambdaMissingIAMPermissionsAnalyzer {
|
|
297
|
+
name = 'LambdaMissingIAMPermissionsAnalyzer';
|
|
298
|
+
async analyze(graph) {
|
|
299
|
+
const findings = [];
|
|
300
|
+
const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
301
|
+
for (const lambda of graph.nodes) {
|
|
302
|
+
if (lambda.type !== 'lambda')
|
|
303
|
+
continue;
|
|
304
|
+
if (!lambda.allowedServices)
|
|
305
|
+
continue; // IAM data unavailable — skip
|
|
306
|
+
if (lambda.allowedServices.includes('*'))
|
|
307
|
+
continue; // AdministratorAccess
|
|
308
|
+
const funcNode = graph.nodes.find((n) => n.type === 'function' && n.name === lambda.name);
|
|
309
|
+
if (!funcNode)
|
|
310
|
+
continue;
|
|
311
|
+
const needed = new Set();
|
|
312
|
+
for (const edge of graph.edges) {
|
|
313
|
+
if (edge.from !== funcNode.id)
|
|
314
|
+
continue;
|
|
315
|
+
const target = nodeMap.get(edge.to);
|
|
316
|
+
if (!target)
|
|
317
|
+
continue;
|
|
318
|
+
if ((edge.type === 'query' || edge.type === 'scan') &&
|
|
319
|
+
target.type === 'table' &&
|
|
320
|
+
target.databaseType === 'dynamodb') {
|
|
321
|
+
needed.add('dynamodb');
|
|
322
|
+
}
|
|
323
|
+
else if (edge.type === 'reads_secret') {
|
|
324
|
+
needed.add('secretsmanager');
|
|
325
|
+
}
|
|
326
|
+
else if (edge.type === 'reads_parameter') {
|
|
327
|
+
needed.add('ssm');
|
|
328
|
+
}
|
|
329
|
+
else if (edge.type === 'publishes_to' && target.type === 'queue') {
|
|
330
|
+
needed.add('sqs');
|
|
331
|
+
}
|
|
332
|
+
else if (edge.type === 'publishes_to' && target.type === 'topic') {
|
|
333
|
+
needed.add('sns');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const service of needed) {
|
|
337
|
+
if (lambda.allowedServices.includes(service))
|
|
338
|
+
continue;
|
|
339
|
+
findings.push({
|
|
340
|
+
severity: 'high',
|
|
341
|
+
issue: `Lambda "${lambda.name}" accesses ${service} but execution role has no ${service} permissions`,
|
|
342
|
+
description: `"${lambda.name}" calls ${service} in code but its IAM execution role (${lambda.roleArn ?? 'unknown'}) has no ${service}:* permissions. This will cause AccessDeniedException at runtime — code passes tests but fails in AWS.`,
|
|
343
|
+
recommendation: `Add ${service} permissions to the execution role for "${lambda.name}". Minimum required: ${MINIMAL_ACTIONS[service] ?? `${service}:*`}.`,
|
|
344
|
+
metadata: { functionName: lambda.name, missingService: service, roleArn: lambda.roleArn },
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return findings;
|
|
349
|
+
}
|
|
350
|
+
}
|
package/dist/analyzers/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer } from './mysql.j
|
|
|
5
5
|
export { MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer } from './mongodb.js';
|
|
6
6
|
export { IaCDriftAnalyzer } from './terraform.js';
|
|
7
7
|
export { PipelineAnalyzer } from './pipeline.js';
|
|
8
|
-
export { MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from './aws-services.js';
|
|
8
|
+
export { MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from './aws-services.js';
|
|
9
9
|
export { RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, } from './rds.js';
|
|
10
10
|
export async function runAllAnalyzers(graph, analyzers) {
|
|
11
11
|
const allFindings = [];
|
|
@@ -13,7 +13,7 @@ import { extractLogsMetadata } from '../../adapters/aws/logs.js';
|
|
|
13
13
|
import { extractS3Metadata } from '../../adapters/aws/s3.js';
|
|
14
14
|
import { scanRepository } from '../../context/index.js';
|
|
15
15
|
import { buildGraph } from '../../graph/index.js';
|
|
16
|
-
import { runAllAnalyzers, IaCDriftAnalyzer, PipelineAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from '../../analyzers/index.js';
|
|
16
|
+
import { runAllAnalyzers, IaCDriftAnalyzer, PipelineAnalyzer, FullTableScanAnalyzer, MissingGSIAnalyzer, HotPartitionAnalyzer, MissingIndexAnalyzer, NplusOneAnalyzer, LargeSelectAnalyzer, MissingMySQLIndexAnalyzer, MySQLFullTableScanAnalyzer, MissingMongoIndexAnalyzer, MongoCollectionScanAnalyzer, MissingDLQAnalyzer, UnencryptedQueueAnalyzer, LargeQueueBacklogAnalyzer, VisibilityTimeoutMismatchAnalyzer, MissingSecretRotationAnalyzer, MissingLogRetentionAnalyzer, LambdaDefaultMemoryAnalyzer, LambdaHighTimeoutAnalyzer, LambdaMissingTriggerDLQAnalyzer, LambdaMissingIAMPermissionsAnalyzer, RDSPubliclyAccessibleAnalyzer, RDSNoBackupAnalyzer, RDSUnencryptedAnalyzer, RDSNoDeletionProtectionAnalyzer, RDSNoMultiAZAnalyzer, S3PublicAccessAnalyzer, S3MissingVersioningAnalyzer, S3UnencryptedAnalyzer, } from '../../analyzers/index.js';
|
|
17
17
|
import { printFinding, printSummaryBox, log, printHeader } from '../utils.js';
|
|
18
18
|
const SEVERITY_ORDER = { high: 3, medium: 2, low: 1, verify: 0 };
|
|
19
19
|
function buildMarkdownReport(findings, projectName) {
|
|
@@ -100,6 +100,7 @@ function buildAnalyzers(config, iacDriftAnalyzer, iacLambdas) {
|
|
|
100
100
|
new LambdaDefaultMemoryAnalyzer(),
|
|
101
101
|
new LambdaHighTimeoutAnalyzer(),
|
|
102
102
|
new LambdaMissingTriggerDLQAnalyzer(),
|
|
103
|
+
new LambdaMissingIAMPermissionsAnalyzer(),
|
|
103
104
|
]
|
|
104
105
|
: []),
|
|
105
106
|
...(config.rds?.enabled === true
|
package/dist/graph/index.js
CHANGED
|
@@ -148,6 +148,8 @@ export function buildGraph(operations, dynamoMeta, postgresMeta, mysqlMeta = [],
|
|
|
148
148
|
timeoutSec: fn.timeoutSec,
|
|
149
149
|
envVarKeys: fn.envVarKeys,
|
|
150
150
|
triggers: fn.triggers,
|
|
151
|
+
roleArn: fn.roleArn,
|
|
152
|
+
allowedServices: fn.allowedServices,
|
|
151
153
|
});
|
|
152
154
|
// Add trigger edges from source → lambda (only for enabled/active mappings)
|
|
153
155
|
for (const trigger of fn.triggers ?? []) {
|
package/dist/server/index.js
CHANGED
|
@@ -132,6 +132,31 @@ export function createMcpServer() {
|
|
|
132
132
|
String(meta?.callerFunctions ?? '').includes(functionName));
|
|
133
133
|
});
|
|
134
134
|
const allTriggers = lambdaNode?.type === 'lambda' ? (lambdaNode.triggers ?? []) : [];
|
|
135
|
+
// Compute missing IAM permissions inline from graph data
|
|
136
|
+
const allowedServices = lambdaNode?.type === 'lambda' ? lambdaNode.allowedServices : undefined;
|
|
137
|
+
let missingPermissions;
|
|
138
|
+
if (allowedServices && !allowedServices.includes('*') && funcNode) {
|
|
139
|
+
const nodeMap = new Map(currentGraph.nodes.map((n) => [n.id, n]));
|
|
140
|
+
const needed = new Set();
|
|
141
|
+
for (const edge of outEdges) {
|
|
142
|
+
const target = nodeMap.get(edge.to);
|
|
143
|
+
if (!target)
|
|
144
|
+
continue;
|
|
145
|
+
if ((edge.type === 'query' || edge.type === 'scan') &&
|
|
146
|
+
target.type === 'table' &&
|
|
147
|
+
target.databaseType === 'dynamodb')
|
|
148
|
+
needed.add('dynamodb');
|
|
149
|
+
else if (edge.type === 'reads_secret')
|
|
150
|
+
needed.add('secretsmanager');
|
|
151
|
+
else if (edge.type === 'reads_parameter')
|
|
152
|
+
needed.add('ssm');
|
|
153
|
+
else if (edge.type === 'publishes_to' && target.type === 'queue')
|
|
154
|
+
needed.add('sqs');
|
|
155
|
+
else if (edge.type === 'publishes_to' && target.type === 'topic')
|
|
156
|
+
needed.add('sns');
|
|
157
|
+
}
|
|
158
|
+
missingPermissions = [...needed].filter((s) => !allowedServices.includes(s));
|
|
159
|
+
}
|
|
135
160
|
return toText({
|
|
136
161
|
function: functionName,
|
|
137
162
|
found: true,
|
|
@@ -151,6 +176,7 @@ export function createMcpServer() {
|
|
|
151
176
|
targetType: target?.type,
|
|
152
177
|
};
|
|
153
178
|
}),
|
|
179
|
+
...(missingPermissions !== undefined ? { missingPermissions } : {}),
|
|
154
180
|
issues: relatedFindings.map((f) => ({
|
|
155
181
|
severity: f.severity,
|
|
156
182
|
issue: f.issue,
|
|
@@ -343,6 +369,7 @@ export function createMcpServer() {
|
|
|
343
369
|
timeoutSec: l.timeoutSec,
|
|
344
370
|
envVarCount: l.envVarKeys?.length ?? 0,
|
|
345
371
|
envVarKeys: l.envVarKeys,
|
|
372
|
+
roleArn: l.roleArn,
|
|
346
373
|
triggers: (l.triggers ?? []).map((t) => ({
|
|
347
374
|
type: t.type,
|
|
348
375
|
source: t.sourceName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infrawise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"mcpName": "io.github.Sidd27/infrawise",
|
|
5
5
|
"description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, S3, API Gateway, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
|
|
6
6
|
"keywords": [
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"@aws-sdk/client-cloudwatch-logs": "^3.1048.0",
|
|
79
79
|
"@aws-sdk/client-dynamodb": "^3.1048.0",
|
|
80
80
|
"@aws-sdk/client-eventbridge": "^3.1051.0",
|
|
81
|
+
"@aws-sdk/client-iam": "^3.1073.0",
|
|
81
82
|
"@aws-sdk/client-lambda": "^3.1048.0",
|
|
82
83
|
"@aws-sdk/client-rds": "^3.1048.0",
|
|
83
84
|
"@aws-sdk/client-s3": "^3.1048.0",
|