@build-astron-co/nimbus 0.2.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/LICENSE +21 -0
- package/README.md +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Discover Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive and non-interactive AWS infrastructure discovery
|
|
5
|
+
*
|
|
6
|
+
* Usage: nimbus aws discover [options]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { writeFile } from 'node:fs/promises';
|
|
10
|
+
import { logger } from '../utils';
|
|
11
|
+
import { RestClient } from '../clients';
|
|
12
|
+
import { createWizard, ui, select, multiSelect, type WizardStep, type StepResult } from '../wizard';
|
|
13
|
+
|
|
14
|
+
// AWS Tools Service client
|
|
15
|
+
const awsToolsUrl = process.env.AWS_TOOLS_SERVICE_URL || 'http://localhost:3009';
|
|
16
|
+
const awsClient = new RestClient(awsToolsUrl);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Discovery context for wizard
|
|
20
|
+
*/
|
|
21
|
+
export interface AwsDiscoverContext {
|
|
22
|
+
// AWS configuration
|
|
23
|
+
awsProfile?: string;
|
|
24
|
+
awsRegions?: string[];
|
|
25
|
+
awsAccountId?: string;
|
|
26
|
+
awsAccountAlias?: string;
|
|
27
|
+
|
|
28
|
+
// Discovery options
|
|
29
|
+
servicesToScan?: string[];
|
|
30
|
+
excludeServices?: string[];
|
|
31
|
+
|
|
32
|
+
// State
|
|
33
|
+
discoverySessionId?: string;
|
|
34
|
+
inventory?: DiscoveryInventory;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Discovery inventory from AWS
|
|
39
|
+
*/
|
|
40
|
+
interface DiscoveryInventory {
|
|
41
|
+
resources: DiscoveredResource[];
|
|
42
|
+
byType: Record<string, number>;
|
|
43
|
+
byRegion: Record<string, number>;
|
|
44
|
+
byService: Record<string, number>;
|
|
45
|
+
summary?: {
|
|
46
|
+
totalResources: number;
|
|
47
|
+
resourcesByService: Record<string, number>;
|
|
48
|
+
resourcesByRegion: Record<string, number>;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Discovered resource
|
|
54
|
+
*/
|
|
55
|
+
interface DiscoveredResource {
|
|
56
|
+
id: string;
|
|
57
|
+
type: string;
|
|
58
|
+
region: string;
|
|
59
|
+
name?: string;
|
|
60
|
+
tags?: Record<string, string>;
|
|
61
|
+
properties: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Command options from CLI arguments
|
|
66
|
+
*/
|
|
67
|
+
export interface AwsDiscoverOptions {
|
|
68
|
+
profile?: string;
|
|
69
|
+
regions?: string[];
|
|
70
|
+
services?: string[];
|
|
71
|
+
excludeServices?: string[];
|
|
72
|
+
outputFormat?: 'json' | 'table' | 'summary';
|
|
73
|
+
outputFile?: string;
|
|
74
|
+
nonInteractive?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Run the AWS discover command
|
|
79
|
+
*/
|
|
80
|
+
export async function awsDiscoverCommand(
|
|
81
|
+
options: AwsDiscoverOptions = {}
|
|
82
|
+
): Promise<DiscoveryInventory | null> {
|
|
83
|
+
logger.info('Starting AWS infrastructure discovery');
|
|
84
|
+
|
|
85
|
+
// Non-interactive mode
|
|
86
|
+
if (options.nonInteractive) {
|
|
87
|
+
return await runNonInteractive(options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Interactive wizard mode
|
|
91
|
+
const wizard = createWizard<AwsDiscoverContext>({
|
|
92
|
+
title: 'nimbus aws discover',
|
|
93
|
+
description: 'Discover AWS infrastructure resources',
|
|
94
|
+
initialContext: {
|
|
95
|
+
awsProfile: options.profile,
|
|
96
|
+
awsRegions: options.regions,
|
|
97
|
+
servicesToScan: options.services,
|
|
98
|
+
excludeServices: options.excludeServices,
|
|
99
|
+
},
|
|
100
|
+
steps: createWizardSteps(),
|
|
101
|
+
onEvent: event => {
|
|
102
|
+
logger.debug('Wizard event', { type: event.type });
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await wizard.run();
|
|
107
|
+
|
|
108
|
+
if (result.success && result.context.inventory) {
|
|
109
|
+
ui.newLine();
|
|
110
|
+
displayInventorySummary(result.context.inventory);
|
|
111
|
+
|
|
112
|
+
// Handle output options
|
|
113
|
+
if (options.outputFile) {
|
|
114
|
+
await saveInventory(
|
|
115
|
+
result.context.inventory,
|
|
116
|
+
options.outputFile,
|
|
117
|
+
options.outputFormat || 'json'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result.context.inventory;
|
|
122
|
+
} else {
|
|
123
|
+
ui.error(`Discovery failed: ${result.error?.message || 'Unknown error'}`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create wizard steps
|
|
130
|
+
*/
|
|
131
|
+
function createWizardSteps(): WizardStep<AwsDiscoverContext>[] {
|
|
132
|
+
return [
|
|
133
|
+
// Step 1: AWS Configuration
|
|
134
|
+
{
|
|
135
|
+
id: 'aws-config',
|
|
136
|
+
title: 'AWS Configuration',
|
|
137
|
+
description: 'Configure AWS profile and regions to scan',
|
|
138
|
+
execute: awsConfigStep,
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// Step 2: Service Selection
|
|
142
|
+
{
|
|
143
|
+
id: 'services',
|
|
144
|
+
title: 'Service Selection',
|
|
145
|
+
description: 'Select which AWS services to scan',
|
|
146
|
+
execute: serviceSelectionStep,
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// Step 3: Discovery
|
|
150
|
+
{
|
|
151
|
+
id: 'discovery',
|
|
152
|
+
title: 'Infrastructure Discovery',
|
|
153
|
+
description: 'Scanning your AWS infrastructure...',
|
|
154
|
+
execute: discoveryStep,
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Step 1: AWS Configuration
|
|
161
|
+
*/
|
|
162
|
+
async function awsConfigStep(ctx: AwsDiscoverContext): Promise<StepResult> {
|
|
163
|
+
// Fetch available profiles
|
|
164
|
+
ui.startSpinner({ message: 'Fetching AWS profiles...' });
|
|
165
|
+
|
|
166
|
+
let profiles: Array<{ name: string; source: string; region?: string; isSSO: boolean }> = [];
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const profilesResponse = await awsClient.get<{
|
|
170
|
+
profiles: Array<{ name: string; source: string; region?: string; isSSO: boolean }>;
|
|
171
|
+
}>('/api/aws/profiles');
|
|
172
|
+
|
|
173
|
+
if (profilesResponse.success && profilesResponse.data?.profiles) {
|
|
174
|
+
profiles = profilesResponse.data.profiles;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
ui.stopSpinnerSuccess(`Found ${profiles.length} AWS profiles`);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
ui.stopSpinnerFail('Could not fetch AWS profiles');
|
|
180
|
+
profiles = [{ name: 'default', source: 'credentials', isSSO: false }];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Profile selection
|
|
184
|
+
let selectedProfile = ctx.awsProfile;
|
|
185
|
+
|
|
186
|
+
if (!selectedProfile) {
|
|
187
|
+
const profileOptions = profiles.map(p => ({
|
|
188
|
+
value: p.name,
|
|
189
|
+
label: p.name + (p.isSSO ? ' (SSO)' : ''),
|
|
190
|
+
description: `Source: ${p.source}${p.region ? `, Region: ${p.region}` : ''}`,
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
selectedProfile = await select({
|
|
194
|
+
message: 'Select AWS profile:',
|
|
195
|
+
options: profileOptions,
|
|
196
|
+
defaultValue: 'default',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (!selectedProfile) {
|
|
200
|
+
return { success: false, error: 'No profile selected' };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Validate credentials
|
|
205
|
+
ui.startSpinner({ message: `Validating credentials for profile "${selectedProfile}"...` });
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const validateResponse = await awsClient.post<{
|
|
209
|
+
valid: boolean;
|
|
210
|
+
accountId?: string;
|
|
211
|
+
accountAlias?: string;
|
|
212
|
+
error?: string;
|
|
213
|
+
}>('/api/aws/profiles/validate', { profile: selectedProfile });
|
|
214
|
+
|
|
215
|
+
if (!validateResponse.success || !validateResponse.data?.valid) {
|
|
216
|
+
ui.stopSpinnerFail(`Invalid credentials: ${validateResponse.data?.error || 'Unknown error'}`);
|
|
217
|
+
return { success: false, error: 'Invalid AWS credentials' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
ui.stopSpinnerSuccess(
|
|
221
|
+
`Authenticated to account ${validateResponse.data.accountId}${
|
|
222
|
+
validateResponse.data.accountAlias ? ` (${validateResponse.data.accountAlias})` : ''
|
|
223
|
+
}`
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
ctx.awsAccountId = validateResponse.data.accountId;
|
|
227
|
+
ctx.awsAccountAlias = validateResponse.data.accountAlias;
|
|
228
|
+
} catch (error: any) {
|
|
229
|
+
ui.stopSpinnerFail(`Failed to validate credentials: ${error.message}`);
|
|
230
|
+
return { success: false, error: 'Credential validation failed' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Region selection
|
|
234
|
+
ui.newLine();
|
|
235
|
+
|
|
236
|
+
const regionChoice = await select<'all' | 'specific'>({
|
|
237
|
+
message: 'Select regions to scan:',
|
|
238
|
+
options: [
|
|
239
|
+
{
|
|
240
|
+
value: 'all',
|
|
241
|
+
label: 'All enabled regions',
|
|
242
|
+
description: 'Scan all regions enabled for your account',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
value: 'specific',
|
|
246
|
+
label: 'Specific regions',
|
|
247
|
+
description: 'Select specific regions to scan',
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
defaultValue: ctx.awsRegions?.length ? 'specific' : 'all',
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
let selectedRegions: string[] = ctx.awsRegions || [];
|
|
254
|
+
|
|
255
|
+
if (regionChoice === 'specific' && selectedRegions.length === 0) {
|
|
256
|
+
// Fetch available regions
|
|
257
|
+
ui.startSpinner({ message: 'Fetching available regions...' });
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const regionsResponse = await awsClient.get<{
|
|
261
|
+
regions: Array<{ name: string; displayName: string }>;
|
|
262
|
+
}>(`/api/aws/regions?profile=${selectedProfile}`);
|
|
263
|
+
|
|
264
|
+
ui.stopSpinnerSuccess(`Found ${regionsResponse.data?.regions?.length || 0} regions`);
|
|
265
|
+
|
|
266
|
+
if (regionsResponse.success && regionsResponse.data?.regions) {
|
|
267
|
+
const regionOptions = regionsResponse.data.regions.map(r => ({
|
|
268
|
+
value: r.name,
|
|
269
|
+
label: `${r.name} - ${r.displayName}`,
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
selectedRegions = (await multiSelect({
|
|
273
|
+
message: 'Select regions to scan:',
|
|
274
|
+
options: regionOptions,
|
|
275
|
+
required: true,
|
|
276
|
+
})) as string[];
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
ui.stopSpinnerFail('Could not fetch regions');
|
|
280
|
+
selectedRegions = ['us-east-1'];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
success: true,
|
|
286
|
+
data: {
|
|
287
|
+
awsProfile: selectedProfile,
|
|
288
|
+
awsRegions: regionChoice === 'all' ? undefined : selectedRegions,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Step 2: Service Selection
|
|
295
|
+
*/
|
|
296
|
+
async function serviceSelectionStep(ctx: AwsDiscoverContext): Promise<StepResult> {
|
|
297
|
+
if (ctx.servicesToScan && ctx.servicesToScan.length > 0) {
|
|
298
|
+
// Services already specified
|
|
299
|
+
ui.info(`Services to scan: ${ctx.servicesToScan.join(', ')}`);
|
|
300
|
+
return { success: true, data: { servicesToScan: ctx.servicesToScan } };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const serviceChoice = await select<'all' | 'specific'>({
|
|
304
|
+
message: 'Select services to scan:',
|
|
305
|
+
options: [
|
|
306
|
+
{
|
|
307
|
+
value: 'all',
|
|
308
|
+
label: 'All supported services',
|
|
309
|
+
description: 'EC2, S3, RDS, Lambda, VPC, IAM, ECS, EKS, DynamoDB, CloudFront',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
value: 'specific',
|
|
313
|
+
label: 'Specific services',
|
|
314
|
+
description: 'Select specific services to scan',
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
defaultValue: 'all',
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (serviceChoice === 'all') {
|
|
321
|
+
return { success: true, data: { servicesToScan: undefined } };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const serviceOptions = [
|
|
325
|
+
{ value: 'EC2', label: 'EC2', description: 'Instances, volumes, security groups, AMIs' },
|
|
326
|
+
{ value: 'S3', label: 'S3', description: 'Buckets and bucket policies' },
|
|
327
|
+
{ value: 'RDS', label: 'RDS', description: 'Database instances and clusters' },
|
|
328
|
+
{ value: 'Lambda', label: 'Lambda', description: 'Functions and layers' },
|
|
329
|
+
{ value: 'VPC', label: 'VPC', description: 'VPCs, subnets, route tables, NAT gateways' },
|
|
330
|
+
{ value: 'IAM', label: 'IAM', description: 'Roles, policies, users, groups' },
|
|
331
|
+
{ value: 'ECS', label: 'ECS', description: 'Clusters, services, task definitions' },
|
|
332
|
+
{ value: 'EKS', label: 'EKS', description: 'Clusters and node groups' },
|
|
333
|
+
{ value: 'DynamoDB', label: 'DynamoDB', description: 'Tables' },
|
|
334
|
+
{ value: 'CloudFront', label: 'CloudFront', description: 'Distributions' },
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
const selectedServices = await multiSelect({
|
|
338
|
+
message: 'Select services to scan:',
|
|
339
|
+
options: serviceOptions,
|
|
340
|
+
required: true,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
success: true,
|
|
345
|
+
data: { servicesToScan: selectedServices as string[] },
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Step 3: Discovery
|
|
351
|
+
*/
|
|
352
|
+
async function discoveryStep(ctx: AwsDiscoverContext): Promise<StepResult> {
|
|
353
|
+
ui.print(' Starting infrastructure discovery...');
|
|
354
|
+
ui.newLine();
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
// Start discovery
|
|
358
|
+
const startResponse = await awsClient.post<{
|
|
359
|
+
sessionId: string;
|
|
360
|
+
status: string;
|
|
361
|
+
}>('/api/aws/discover', {
|
|
362
|
+
profile: ctx.awsProfile,
|
|
363
|
+
regions: ctx.awsRegions || 'all',
|
|
364
|
+
services: ctx.servicesToScan,
|
|
365
|
+
excludeServices: ctx.excludeServices,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (!startResponse.success || !startResponse.data?.sessionId) {
|
|
369
|
+
return { success: false, error: 'Failed to start discovery' };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const sessionId = startResponse.data.sessionId;
|
|
373
|
+
ctx.discoverySessionId = sessionId;
|
|
374
|
+
|
|
375
|
+
// Poll for progress with visual feedback
|
|
376
|
+
let completed = false;
|
|
377
|
+
let lastUpdate = '';
|
|
378
|
+
|
|
379
|
+
while (!completed) {
|
|
380
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
381
|
+
|
|
382
|
+
const statusResponse = await awsClient.get<{
|
|
383
|
+
status: string;
|
|
384
|
+
progress: {
|
|
385
|
+
regionsScanned: number;
|
|
386
|
+
totalRegions: number;
|
|
387
|
+
servicesScanned: number;
|
|
388
|
+
totalServices: number;
|
|
389
|
+
resourcesFound: number;
|
|
390
|
+
currentRegion?: string;
|
|
391
|
+
currentService?: string;
|
|
392
|
+
errors: string[];
|
|
393
|
+
};
|
|
394
|
+
inventory?: DiscoveryInventory;
|
|
395
|
+
}>(`/api/aws/discover/${sessionId}`);
|
|
396
|
+
|
|
397
|
+
if (!statusResponse.success) {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const { status, progress, inventory } = statusResponse.data!;
|
|
402
|
+
|
|
403
|
+
// Build progress message
|
|
404
|
+
const progressMsg = buildProgressMessage(progress);
|
|
405
|
+
if (progressMsg !== lastUpdate) {
|
|
406
|
+
ui.clearLine();
|
|
407
|
+
ui.write(progressMsg);
|
|
408
|
+
lastUpdate = progressMsg;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (status === 'completed') {
|
|
412
|
+
completed = true;
|
|
413
|
+
ctx.inventory = inventory;
|
|
414
|
+
|
|
415
|
+
ui.newLine();
|
|
416
|
+
ui.newLine();
|
|
417
|
+
ui.success(`Discovery complete! Found ${progress.resourcesFound} resources`);
|
|
418
|
+
|
|
419
|
+
if (progress.errors.length > 0) {
|
|
420
|
+
ui.newLine();
|
|
421
|
+
ui.warning(`${progress.errors.length} errors occurred during discovery:`);
|
|
422
|
+
for (const err of progress.errors.slice(0, 5)) {
|
|
423
|
+
ui.print(` ${ui.dim(err)}`);
|
|
424
|
+
}
|
|
425
|
+
if (progress.errors.length > 5) {
|
|
426
|
+
ui.print(` ${ui.dim(`... and ${progress.errors.length - 5} more`)}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} else if (status === 'failed') {
|
|
430
|
+
ui.newLine();
|
|
431
|
+
ui.error('Discovery failed');
|
|
432
|
+
return { success: false, error: 'Discovery failed' };
|
|
433
|
+
} else if (status === 'cancelled') {
|
|
434
|
+
ui.newLine();
|
|
435
|
+
ui.warning('Discovery was cancelled');
|
|
436
|
+
return { success: false, error: 'Discovery cancelled' };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
success: true,
|
|
442
|
+
data: {
|
|
443
|
+
discoverySessionId: sessionId,
|
|
444
|
+
inventory: ctx.inventory,
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
} catch (error: any) {
|
|
448
|
+
return { success: false, error: error.message };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Build progress message
|
|
454
|
+
*/
|
|
455
|
+
function buildProgressMessage(progress: {
|
|
456
|
+
regionsScanned: number;
|
|
457
|
+
totalRegions: number;
|
|
458
|
+
servicesScanned: number;
|
|
459
|
+
totalServices: number;
|
|
460
|
+
resourcesFound: number;
|
|
461
|
+
currentRegion?: string;
|
|
462
|
+
currentService?: string;
|
|
463
|
+
}): string {
|
|
464
|
+
const parts = [
|
|
465
|
+
` Regions: ${progress.regionsScanned}/${progress.totalRegions}`,
|
|
466
|
+
`Services: ${progress.servicesScanned}/${progress.totalServices}`,
|
|
467
|
+
`Resources: ${progress.resourcesFound}`,
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
if (progress.currentRegion && progress.currentService) {
|
|
471
|
+
parts.push(`Current: ${progress.currentRegion}/${progress.currentService}`);
|
|
472
|
+
} else if (progress.currentRegion) {
|
|
473
|
+
parts.push(`Current: ${progress.currentRegion}`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return parts.join(' | ');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Display inventory summary
|
|
481
|
+
*/
|
|
482
|
+
function displayInventorySummary(inventory: DiscoveryInventory): void {
|
|
483
|
+
ui.box({
|
|
484
|
+
title: 'Discovery Summary',
|
|
485
|
+
content: [
|
|
486
|
+
`Total Resources: ${inventory.resources.length}`,
|
|
487
|
+
'',
|
|
488
|
+
'By Service:',
|
|
489
|
+
...Object.entries(inventory.byService || {}).map(
|
|
490
|
+
([service, count]) => ` ${service}: ${count}`
|
|
491
|
+
),
|
|
492
|
+
'',
|
|
493
|
+
'By Region:',
|
|
494
|
+
...Object.entries(inventory.byRegion || {}).map(([region, count]) => ` ${region}: ${count}`),
|
|
495
|
+
],
|
|
496
|
+
style: 'rounded',
|
|
497
|
+
borderColor: 'cyan',
|
|
498
|
+
padding: 1,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Save inventory to file
|
|
504
|
+
*/
|
|
505
|
+
async function saveInventory(
|
|
506
|
+
inventory: DiscoveryInventory,
|
|
507
|
+
outputFile: string,
|
|
508
|
+
format: 'json' | 'table' | 'summary'
|
|
509
|
+
): Promise<void> {
|
|
510
|
+
let content: string;
|
|
511
|
+
|
|
512
|
+
if (format === 'json') {
|
|
513
|
+
content = JSON.stringify(inventory, null, 2);
|
|
514
|
+
} else if (format === 'summary') {
|
|
515
|
+
content = [
|
|
516
|
+
`# AWS Infrastructure Discovery Summary`,
|
|
517
|
+
``,
|
|
518
|
+
`Total Resources: ${inventory.resources.length}`,
|
|
519
|
+
``,
|
|
520
|
+
`## By Service`,
|
|
521
|
+
...Object.entries(inventory.byService || {}).map(
|
|
522
|
+
([service, count]) => `- ${service}: ${count}`
|
|
523
|
+
),
|
|
524
|
+
``,
|
|
525
|
+
`## By Region`,
|
|
526
|
+
...Object.entries(inventory.byRegion || {}).map(([region, count]) => `- ${region}: ${count}`),
|
|
527
|
+
].join('\n');
|
|
528
|
+
} else {
|
|
529
|
+
// Table format - CSV-like
|
|
530
|
+
const headers = ['ID', 'Type', 'Region', 'Name'];
|
|
531
|
+
const rows = inventory.resources.map(r => [r.id, r.type, r.region, r.name || '']);
|
|
532
|
+
content = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
await writeFile(outputFile, content, 'utf-8');
|
|
536
|
+
ui.success(`Inventory saved to ${outputFile}`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Run in non-interactive mode
|
|
541
|
+
*/
|
|
542
|
+
async function runNonInteractive(options: AwsDiscoverOptions): Promise<DiscoveryInventory | null> {
|
|
543
|
+
ui.header('nimbus aws discover', 'Non-interactive mode');
|
|
544
|
+
|
|
545
|
+
// Validate required options
|
|
546
|
+
if (!options.profile) {
|
|
547
|
+
ui.error('Profile is required in non-interactive mode (--profile)');
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
ui.info(`Using profile: ${options.profile}`);
|
|
552
|
+
ui.info(`Regions: ${options.regions?.join(', ') || 'all'}`);
|
|
553
|
+
ui.info(`Services: ${options.services?.join(', ') || 'all'}`);
|
|
554
|
+
|
|
555
|
+
// Validate credentials
|
|
556
|
+
ui.startSpinner({ message: 'Validating credentials...' });
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const validateResponse = await awsClient.post<{
|
|
560
|
+
valid: boolean;
|
|
561
|
+
accountId?: string;
|
|
562
|
+
error?: string;
|
|
563
|
+
}>('/api/aws/profiles/validate', { profile: options.profile });
|
|
564
|
+
|
|
565
|
+
if (!validateResponse.success || !validateResponse.data?.valid) {
|
|
566
|
+
ui.stopSpinnerFail(`Invalid credentials: ${validateResponse.data?.error || 'Unknown error'}`);
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
ui.stopSpinnerSuccess(`Authenticated to account ${validateResponse.data.accountId}`);
|
|
571
|
+
} catch (error: any) {
|
|
572
|
+
ui.stopSpinnerFail(`Credential validation failed: ${error.message}`);
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Start discovery
|
|
577
|
+
ui.startSpinner({ message: 'Starting discovery...' });
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
const startResponse = await awsClient.post<{
|
|
581
|
+
sessionId: string;
|
|
582
|
+
}>('/api/aws/discover', {
|
|
583
|
+
profile: options.profile,
|
|
584
|
+
regions: options.regions || 'all',
|
|
585
|
+
services: options.services,
|
|
586
|
+
excludeServices: options.excludeServices,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
if (!startResponse.success || !startResponse.data?.sessionId) {
|
|
590
|
+
ui.stopSpinnerFail('Failed to start discovery');
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const sessionId = startResponse.data.sessionId;
|
|
595
|
+
ui.stopSpinnerSuccess(`Discovery started (session: ${sessionId})`);
|
|
596
|
+
|
|
597
|
+
// Poll for completion
|
|
598
|
+
ui.startSpinner({ message: 'Scanning infrastructure...' });
|
|
599
|
+
|
|
600
|
+
let completed = false;
|
|
601
|
+
let inventory: DiscoveryInventory | undefined;
|
|
602
|
+
|
|
603
|
+
while (!completed) {
|
|
604
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
605
|
+
|
|
606
|
+
const statusResponse = await awsClient.get<{
|
|
607
|
+
status: string;
|
|
608
|
+
progress: { resourcesFound: number };
|
|
609
|
+
inventory?: DiscoveryInventory;
|
|
610
|
+
}>(`/api/aws/discover/${sessionId}`);
|
|
611
|
+
|
|
612
|
+
if (!statusResponse.success) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const { status, progress } = statusResponse.data!;
|
|
617
|
+
|
|
618
|
+
ui.updateSpinner(`Scanning... ${progress.resourcesFound} resources found`);
|
|
619
|
+
|
|
620
|
+
if (status === 'completed') {
|
|
621
|
+
completed = true;
|
|
622
|
+
inventory = statusResponse.data!.inventory;
|
|
623
|
+
ui.stopSpinnerSuccess(`Discovery complete! Found ${progress.resourcesFound} resources`);
|
|
624
|
+
} else if (status === 'failed' || status === 'cancelled') {
|
|
625
|
+
ui.stopSpinnerFail(`Discovery ${status}`);
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (inventory) {
|
|
631
|
+
// Display summary
|
|
632
|
+
displayInventorySummary(inventory);
|
|
633
|
+
|
|
634
|
+
// Save to file if requested
|
|
635
|
+
if (options.outputFile) {
|
|
636
|
+
await saveInventory(inventory, options.outputFile, options.outputFormat || 'json');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return inventory;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return null;
|
|
643
|
+
} catch (error: any) {
|
|
644
|
+
ui.stopSpinnerFail(`Discovery failed: ${error.message}`);
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export default awsDiscoverCommand;
|