auramaxx 1.0.0-alpha.4
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 +26 -0
- package/README.md +112 -0
- package/bin/aurawallet.js +121 -0
- package/docs/ADAPTERS.md +467 -0
- package/docs/API.md +2679 -0
- package/docs/APPS.md +198 -0
- package/docs/ARCHITECTURE.md +350 -0
- package/docs/AUTH.md +698 -0
- package/docs/BEST-PRACTICES.md +121 -0
- package/docs/CLI.md +61 -0
- package/docs/DEVELOPING-APPS.md +452 -0
- package/docs/EXTENSION.md +97 -0
- package/docs/JOBS.md +33 -0
- package/docs/MCP.md +76 -0
- package/docs/PROTOCOL.md +142 -0
- package/docs/SETUP.md +219 -0
- package/docs/WORKSPACE.md +672 -0
- package/docs/agent-auth.md +63 -0
- package/docs/aura-file.md +48 -0
- package/docs/credentials.md +53 -0
- package/docs/external/getting-started.md +65 -0
- package/docs/external/overview.md +45 -0
- package/docs/external/use-cases.md +48 -0
- package/docs/external/why-aura.md +35 -0
- package/docs/jobs/connect-agent.md +77 -0
- package/docs/jobs/migrate-from-dotenv.md +79 -0
- package/docs/jobs/recover-from-lockout.md +72 -0
- package/docs/jobs/secure-ci.md +63 -0
- package/docs/oauth2.md +42 -0
- package/docs/passkeys.md +60 -0
- package/docs/security.md +540 -0
- package/docs/specs/aura-open-protocol.md +61 -0
- package/docs/specs/aura-provider-plugin.md +24 -0
- package/docs/specs/aura-registry-model.md +31 -0
- package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
- package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
- package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
- package/docs/specs/fixtures/valid-basic.aura +4 -0
- package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
- package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
- package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
- package/docs/totp.md +40 -0
- package/docs/wallet/AI.md +508 -0
- package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
- package/docs/wallet/README.md +47 -0
- package/docs/wallet/STRATEGY.md +89 -0
- package/next.config.ts +21 -0
- package/package.json +151 -0
- package/postcss.config.mjs +8 -0
- package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
- package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
- package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +447 -0
- package/public/logo-chevron.svg +31 -0
- package/public/logo-concentric.svg +31 -0
- package/public/logo-crosshatch.svg +39 -0
- package/public/logo-dashed.svg +39 -0
- package/public/logo-horizontal.svg +31 -0
- package/public/logo-m56.svg +64 -0
- package/public/logo.webp +0 -0
- package/scripts/add-app.js +245 -0
- package/scripts/init.sh +57 -0
- package/scripts/migrate-apikeys-to-credentials.ts +35 -0
- package/scripts/sandbox-agent-flow.sh +235 -0
- package/scripts/sandbox.sh +175 -0
- package/scripts/validate-job-docs.mjs +125 -0
- package/server/abi/SwapHelper.json +438 -0
- package/server/cli/approval.ts +447 -0
- package/server/cli/commands/app.ts +204 -0
- package/server/cli/commands/cron.ts +24 -0
- package/server/cli/commands/doctor.ts +1007 -0
- package/server/cli/commands/env.ts +456 -0
- package/server/cli/commands/init.ts +752 -0
- package/server/cli/commands/mcp.ts +125 -0
- package/server/cli/commands/restore.ts +314 -0
- package/server/cli/commands/shell-hook.ts +468 -0
- package/server/cli/commands/start.ts +62 -0
- package/server/cli/commands/status.ts +59 -0
- package/server/cli/commands/stop.ts +14 -0
- package/server/cli/commands/token.ts +180 -0
- package/server/cli/commands/unlock.ts +49 -0
- package/server/cli/commands/vault.ts +417 -0
- package/server/cli/index.ts +328 -0
- package/server/cli/lib/aura-parser.ts +64 -0
- package/server/cli/lib/credential-create.ts +74 -0
- package/server/cli/lib/credential-resolve.ts +254 -0
- package/server/cli/lib/dotenv-migrate.ts +116 -0
- package/server/cli/lib/dotenv-parser.ts +146 -0
- package/server/cli/lib/http.ts +91 -0
- package/server/cli/lib/init-steps.ts +76 -0
- package/server/cli/lib/local-agent-trust.ts +45 -0
- package/server/cli/lib/process.ts +136 -0
- package/server/cli/lib/prompt.ts +85 -0
- package/server/cli/lib/theme.ts +240 -0
- package/server/cli/socket.ts +570 -0
- package/server/cli/transport-client.ts +50 -0
- package/server/cron/index.ts +137 -0
- package/server/cron/job.ts +31 -0
- package/server/cron/jobs/balance-sync.ts +436 -0
- package/server/cron/jobs/incoming-scan.ts +506 -0
- package/server/cron/jobs/native-price.ts +70 -0
- package/server/cron/jobs/orphan-cleanup.ts +40 -0
- package/server/cron/jobs/strategy-runner.ts +175 -0
- package/server/cron/scheduler.ts +125 -0
- package/server/index.ts +406 -0
- package/server/lib/adapters/factory.ts +110 -0
- package/server/lib/adapters/index.ts +19 -0
- package/server/lib/adapters/router.ts +297 -0
- package/server/lib/adapters/telegram.ts +645 -0
- package/server/lib/adapters/types.ts +89 -0
- package/server/lib/adapters/webhook.ts +95 -0
- package/server/lib/address.ts +49 -0
- package/server/lib/agent-auth/contracts.ts +1194 -0
- package/server/lib/agent-profiles.ts +328 -0
- package/server/lib/ai.ts +285 -0
- package/server/lib/api-registry/contracts.ts +86 -0
- package/server/lib/api-registry/validation.ts +172 -0
- package/server/lib/apikey-migration.ts +189 -0
- package/server/lib/app-installer.ts +505 -0
- package/server/lib/app-tokens.ts +247 -0
- package/server/lib/auth.ts +314 -0
- package/server/lib/batch.ts +242 -0
- package/server/lib/cold.ts +874 -0
- package/server/lib/config.ts +381 -0
- package/server/lib/credential-access-audit.ts +85 -0
- package/server/lib/credential-access-policy.ts +110 -0
- package/server/lib/credential-health.ts +343 -0
- package/server/lib/credential-import.ts +487 -0
- package/server/lib/credential-scope.ts +87 -0
- package/server/lib/credential-shares.ts +190 -0
- package/server/lib/credential-transport.ts +342 -0
- package/server/lib/credential-vault.ts +77 -0
- package/server/lib/credentials.ts +333 -0
- package/server/lib/crypto.ts +8 -0
- package/server/lib/db.ts +15 -0
- package/server/lib/defaults.ts +366 -0
- package/server/lib/dex/index.ts +80 -0
- package/server/lib/dex/relay.ts +235 -0
- package/server/lib/dex/types.ts +59 -0
- package/server/lib/dex/uniswap.ts +370 -0
- package/server/lib/e2e-agent/artifacts.ts +36 -0
- package/server/lib/e2e-agent/contracts.ts +112 -0
- package/server/lib/e2e-agent/validation.ts +135 -0
- package/server/lib/encrypt.ts +128 -0
- package/server/lib/error.ts +20 -0
- package/server/lib/events.ts +205 -0
- package/server/lib/hot.ts +357 -0
- package/server/lib/key-fingerprint.ts +28 -0
- package/server/lib/logger.ts +331 -0
- package/server/lib/network.ts +137 -0
- package/server/lib/notifications.ts +219 -0
- package/server/lib/oauth2-refresh.ts +241 -0
- package/server/lib/oursecret.ts +54 -0
- package/server/lib/passkey-credential.ts +360 -0
- package/server/lib/passkey.ts +68 -0
- package/server/lib/permissions.ts +248 -0
- package/server/lib/pino.ts +24 -0
- package/server/lib/policy-preview.ts +138 -0
- package/server/lib/price.ts +338 -0
- package/server/lib/prices.ts +34 -0
- package/server/lib/project-scope.ts +239 -0
- package/server/lib/resolve-action.ts +427 -0
- package/server/lib/resolve.ts +36 -0
- package/server/lib/sessions.ts +632 -0
- package/server/lib/solana/connection.ts +26 -0
- package/server/lib/solana/jupiter.ts +128 -0
- package/server/lib/solana/transfer.ts +108 -0
- package/server/lib/solana/wallet.ts +136 -0
- package/server/lib/strategy/emits.ts +21 -0
- package/server/lib/strategy/engine.ts +1305 -0
- package/server/lib/strategy/executor.ts +115 -0
- package/server/lib/strategy/hook-context.ts +158 -0
- package/server/lib/strategy/hooks.ts +990 -0
- package/server/lib/strategy/index.ts +28 -0
- package/server/lib/strategy/installer.ts +305 -0
- package/server/lib/strategy/loader.ts +256 -0
- package/server/lib/strategy/message.ts +235 -0
- package/server/lib/strategy/repository.ts +218 -0
- package/server/lib/strategy/session-logger.ts +693 -0
- package/server/lib/strategy/sources.ts +288 -0
- package/server/lib/strategy/state.ts +189 -0
- package/server/lib/strategy/templates.ts +403 -0
- package/server/lib/strategy/tick.ts +404 -0
- package/server/lib/strategy/types.ts +230 -0
- package/server/lib/swap.ts +3 -0
- package/server/lib/temp.ts +86 -0
- package/server/lib/token-metadata.ts +86 -0
- package/server/lib/token-safety.ts +200 -0
- package/server/lib/token-search.ts +444 -0
- package/server/lib/totp.ts +194 -0
- package/server/lib/transactions.ts +123 -0
- package/server/lib/transport.ts +75 -0
- package/server/lib/txhistory/decoder.ts +262 -0
- package/server/lib/txhistory/enricher.ts +652 -0
- package/server/lib/txhistory/index.ts +391 -0
- package/server/lib/txhistory/signatures.ts +59 -0
- package/server/lib/verified-summary.ts +421 -0
- package/server/mcp/profile-policy.ts +30 -0
- package/server/mcp/server.ts +619 -0
- package/server/mcp/tools.ts +523 -0
- package/server/middleware/auth.ts +119 -0
- package/server/middleware/requestLogger.ts +84 -0
- package/server/routes/actions.ts +459 -0
- package/server/routes/adapters.ts +703 -0
- package/server/routes/addressbook.ts +113 -0
- package/server/routes/ai.ts +34 -0
- package/server/routes/apikeys.ts +295 -0
- package/server/routes/apps.ts +601 -0
- package/server/routes/auth.ts +457 -0
- package/server/routes/backup.ts +340 -0
- package/server/routes/batch.ts +270 -0
- package/server/routes/bookmarks.ts +162 -0
- package/server/routes/credential-shares.ts +198 -0
- package/server/routes/credential-vaults.ts +154 -0
- package/server/routes/credentials.ts +1290 -0
- package/server/routes/dashboard.ts +71 -0
- package/server/routes/defaults.ts +124 -0
- package/server/routes/fund.ts +229 -0
- package/server/routes/import.ts +352 -0
- package/server/routes/launch.ts +665 -0
- package/server/routes/lock.ts +54 -0
- package/server/routes/logs.ts +68 -0
- package/server/routes/nuke.ts +111 -0
- package/server/routes/passkey-credentials.ts +99 -0
- package/server/routes/passkey.ts +346 -0
- package/server/routes/portfolio.ts +217 -0
- package/server/routes/price.ts +63 -0
- package/server/routes/resolve.ts +31 -0
- package/server/routes/security.ts +45 -0
- package/server/routes/send-evm.ts +241 -0
- package/server/routes/send-solana.ts +281 -0
- package/server/routes/send.ts +178 -0
- package/server/routes/setup.ts +210 -0
- package/server/routes/strategy.ts +894 -0
- package/server/routes/swap-evm.ts +353 -0
- package/server/routes/swap-solana.ts +177 -0
- package/server/routes/swap.ts +356 -0
- package/server/routes/token.ts +247 -0
- package/server/routes/unlock.ts +403 -0
- package/server/routes/wallet-assets.ts +361 -0
- package/server/routes/wallet-transactions.ts +515 -0
- package/server/routes/wallet.ts +710 -0
- package/server/types.ts +146 -0
- package/skills/aurawallet/SKILL.md +739 -0
- package/skills/aurawallet-setup/SKILL.md +74 -0
- package/skills/security-review/SKILL.md +148 -0
- package/src/app/api/agent-requests/route.ts +30 -0
- package/src/app/api/apps/install/route.ts +126 -0
- package/src/app/api/apps/manifests/route.ts +16 -0
- package/src/app/api/apps/static/[...path]/route.ts +57 -0
- package/src/app/api/events/route.ts +92 -0
- package/src/app/api/page.tsx +212 -0
- package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
- package/src/app/api/workspace/[id]/apps/route.ts +81 -0
- package/src/app/api/workspace/[id]/export/route.ts +67 -0
- package/src/app/api/workspace/[id]/route.ts +168 -0
- package/src/app/api/workspace/auth.ts +34 -0
- package/src/app/api/workspace/config/route.ts +106 -0
- package/src/app/api/workspace/import/route.ts +127 -0
- package/src/app/api/workspace/route.ts +116 -0
- package/src/app/app/page.tsx +2122 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/docs/page.tsx +178 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +572 -0
- package/src/app/health/page.tsx +5 -0
- package/src/app/hello/page.tsx +15 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +986 -0
- package/src/app/providers.tsx +90 -0
- package/src/app/share/[token]/page.tsx +295 -0
- package/src/components/ChainSelector.tsx +144 -0
- package/src/components/HumanActionBar.tsx +695 -0
- package/src/components/NotificationDrawer.tsx +129 -0
- package/src/components/apps/AgentKeysApp.tsx +490 -0
- package/src/components/apps/App.tsx +153 -0
- package/src/components/apps/AppGrid.tsx +15 -0
- package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
- package/src/components/apps/DraggableApp.tsx +562 -0
- package/src/components/apps/IFrameApp.tsx +73 -0
- package/src/components/apps/LogsApp.tsx +360 -0
- package/src/components/apps/SendApp.tsx +394 -0
- package/src/components/apps/SetupWizardApp.tsx +1004 -0
- package/src/components/apps/SystemDefaultsApp.tsx +845 -0
- package/src/components/apps/ThirdPartyApp.tsx +428 -0
- package/src/components/apps/TokenApp.tsx +319 -0
- package/src/components/apps/TransactionsApp.tsx +438 -0
- package/src/components/apps/WalletDetailApp.tsx +1505 -0
- package/src/components/apps/index.ts +13 -0
- package/src/components/design-system/Button.tsx +53 -0
- package/src/components/design-system/ChainIndicator.tsx +65 -0
- package/src/components/design-system/ChainSelector.tsx +137 -0
- package/src/components/design-system/ConfirmationModal.tsx +106 -0
- package/src/components/design-system/ConfirmationPopover.tsx +81 -0
- package/src/components/design-system/Drawer.tsx +123 -0
- package/src/components/design-system/FilterDropdown.tsx +72 -0
- package/src/components/design-system/Modal.tsx +206 -0
- package/src/components/design-system/Popover.tsx +142 -0
- package/src/components/design-system/TextInput.tsx +85 -0
- package/src/components/design-system/Toggle.tsx +58 -0
- package/src/components/design-system/index.ts +11 -0
- package/src/components/docs/DocsThemeToggle.tsx +49 -0
- package/src/components/health/CredentialHealthDashboard.tsx +214 -0
- package/src/components/icons/ChainIcons.tsx +72 -0
- package/src/components/layout/AppStoreDrawer.tsx +369 -0
- package/src/components/layout/ContentArea.tsx +21 -0
- package/src/components/layout/TabBar.tsx +278 -0
- package/src/components/layout/WalletSidebar.tsx +1033 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
- package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
- package/src/components/vault/ApiKeysConsole.tsx +1080 -0
- package/src/components/vault/AuditConsole.tsx +584 -0
- package/src/components/vault/CredentialDetail.tsx +455 -0
- package/src/components/vault/CredentialEmpty.tsx +55 -0
- package/src/components/vault/CredentialField.tsx +361 -0
- package/src/components/vault/CredentialForm.tsx +1212 -0
- package/src/components/vault/CredentialList.tsx +165 -0
- package/src/components/vault/CredentialRow.tsx +97 -0
- package/src/components/vault/CredentialShareModal.tsx +178 -0
- package/src/components/vault/CredentialVault.tsx +754 -0
- package/src/components/vault/CredentialWalletWidget.tsx +103 -0
- package/src/components/vault/ImportCredentialsModal.tsx +515 -0
- package/src/components/vault/LargeTypeModal.tsx +64 -0
- package/src/components/vault/PasswordGenerator.tsx +224 -0
- package/src/components/vault/TOTPDisplay.tsx +123 -0
- package/src/components/vault/VaultSidebar.tsx +413 -0
- package/src/components/vault/types.ts +54 -0
- package/src/context/AuthContext.tsx +337 -0
- package/src/context/PriceContext.tsx +113 -0
- package/src/context/ThemeContext.tsx +164 -0
- package/src/context/WebSocketContext.tsx +269 -0
- package/src/context/WorkspaceContext.tsx +668 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAgentActions.ts +368 -0
- package/src/hooks/useBalance.ts +103 -0
- package/src/hooks/useBalances.ts +129 -0
- package/src/instrumentation.ts +12 -0
- package/src/lib/api.ts +449 -0
- package/src/lib/app-loader.ts +148 -0
- package/src/lib/app-registry.ts +178 -0
- package/src/lib/app-sdk.ts +157 -0
- package/src/lib/audit-console-adapter.ts +151 -0
- package/src/lib/auth-client.ts +75 -0
- package/src/lib/config.ts +74 -0
- package/src/lib/crypto.ts +112 -0
- package/src/lib/db.ts +21 -0
- package/src/lib/docs.ts +390 -0
- package/src/lib/events.ts +361 -0
- package/src/lib/pino.ts +24 -0
- package/src/lib/theme-handlers.ts +168 -0
- package/src/lib/theme.ts +351 -0
- package/src/lib/tokenData.ts +378 -0
- package/src/lib/vault-crypto.ts +129 -0
- package/src/lib/websocket-server.ts +302 -0
- package/src/lib/websocket-setup.ts +79 -0
- package/src/lib/wordlist.ts +2050 -0
- package/src/lib/workspace-handlers.ts +285 -0
- package/start.sh +80 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aurawallet doctor — deterministic onboarding/runtime diagnostics
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import net from 'net';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import { spawnSync } from 'child_process';
|
|
10
|
+
import { fetchJson, serverUrl, type SetupStatus } from '../lib/http';
|
|
11
|
+
import { parseAuraFile, type AuraMapping } from '../lib/aura-parser';
|
|
12
|
+
import { getErrorMessage } from '../../lib/error';
|
|
13
|
+
import { printBanner, checkBadge, printSection } from '../lib/theme';
|
|
14
|
+
|
|
15
|
+
export type CheckStatus = 'pass' | 'warn' | 'fail';
|
|
16
|
+
export type CheckSeverity = 'info' | 'low' | 'medium' | 'high' | 'critical';
|
|
17
|
+
|
|
18
|
+
export interface DoctorCheck {
|
|
19
|
+
id: string;
|
|
20
|
+
code: string;
|
|
21
|
+
severity: CheckSeverity;
|
|
22
|
+
status: CheckStatus;
|
|
23
|
+
finding: string;
|
|
24
|
+
evidence: string;
|
|
25
|
+
remediation: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface DoctorResult {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
mode: 'default' | 'strict';
|
|
31
|
+
summary: { pass: number; warn: number; fail: number };
|
|
32
|
+
checks: DoctorCheck[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DoctorOptions {
|
|
36
|
+
json: boolean;
|
|
37
|
+
strict: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CredentialHealthSummary {
|
|
41
|
+
totalAnalyzed: number;
|
|
42
|
+
safe: number;
|
|
43
|
+
weak: number;
|
|
44
|
+
reused: number;
|
|
45
|
+
breached: number;
|
|
46
|
+
unknown: number;
|
|
47
|
+
lastScanAt: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface TokenValidateResponse {
|
|
51
|
+
valid: boolean;
|
|
52
|
+
error?: string;
|
|
53
|
+
payload?: {
|
|
54
|
+
permissions?: string[];
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface AuthProbeState {
|
|
59
|
+
socketViable: boolean;
|
|
60
|
+
tokenPresent: boolean;
|
|
61
|
+
tokenShapeValid: boolean;
|
|
62
|
+
tokenMask: string;
|
|
63
|
+
tokenValidate?: TokenValidateResponse;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const EXIT = {
|
|
67
|
+
OK: 0,
|
|
68
|
+
FAIL: 1,
|
|
69
|
+
INTERNAL: 2,
|
|
70
|
+
ARGS: 3,
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
const MAX_SECURITY_ENTRIES = 200;
|
|
74
|
+
const SECURITY_TIME_BUDGET_MS = 2000;
|
|
75
|
+
const MAX_AURA_MAPPINGS = 100;
|
|
76
|
+
const MAX_UNIQUE_CREDENTIAL_PROBES = 25;
|
|
77
|
+
const HTTP_TIMEOUT_MS = 1500;
|
|
78
|
+
|
|
79
|
+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
80
|
+
return new Promise<T>((resolve, reject) => {
|
|
81
|
+
const timer = setTimeout(() => reject(new Error(`${label}-timeout`)), timeoutMs);
|
|
82
|
+
promise
|
|
83
|
+
.then((value) => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
resolve(value);
|
|
86
|
+
})
|
|
87
|
+
.catch((err) => {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
reject(err);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseArgs(argv: string[]): DoctorOptions {
|
|
95
|
+
const flags = new Set(argv);
|
|
96
|
+
for (const flag of flags) {
|
|
97
|
+
if (!['--json', '--strict', '--help', '-h'].includes(flag)) {
|
|
98
|
+
throw new Error(`Unknown flag: ${flag}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (flags.has('--help') || flags.has('-h')) {
|
|
103
|
+
console.log('Usage: npx aurawallet doctor [--json] [--strict]');
|
|
104
|
+
process.exit(EXIT.OK);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
json: flags.has('--json'),
|
|
109
|
+
strict: flags.has('--strict'),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function maskToken(token?: string): string {
|
|
114
|
+
if (!token) return 'absent';
|
|
115
|
+
const trimmed = token.trim();
|
|
116
|
+
if (!trimmed) return 'present-empty';
|
|
117
|
+
return `tok_****${trimmed.slice(-4)}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function checkTokenShape(token?: string): boolean {
|
|
121
|
+
if (!token) return false;
|
|
122
|
+
return token.trim().length >= 16;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function probeSocketViability(): Promise<{ viable: boolean; evidence: string }> {
|
|
126
|
+
const uid = process.getuid?.() ?? os.userInfo().uid;
|
|
127
|
+
const socketPath = `/tmp/aura-cli-${uid}.sock`;
|
|
128
|
+
|
|
129
|
+
let st: fs.Stats;
|
|
130
|
+
try {
|
|
131
|
+
st = fs.statSync(socketPath);
|
|
132
|
+
} catch {
|
|
133
|
+
return { viable: false, evidence: 'socket-missing' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!st.isSocket()) return { viable: false, evidence: 'socket-path-not-unix-socket' };
|
|
137
|
+
if (st.uid !== uid) return { viable: false, evidence: 'socket-owner-mismatch' };
|
|
138
|
+
if ((st.mode & 0o777) > 0o600) return { viable: false, evidence: 'socket-perms-too-open' };
|
|
139
|
+
|
|
140
|
+
const connectOk = await new Promise<boolean>((resolve) => {
|
|
141
|
+
const client = net.createConnection(socketPath);
|
|
142
|
+
const timer = setTimeout(() => {
|
|
143
|
+
client.destroy();
|
|
144
|
+
resolve(false);
|
|
145
|
+
}, 500);
|
|
146
|
+
|
|
147
|
+
let done = false;
|
|
148
|
+
const finish = (value: boolean) => {
|
|
149
|
+
if (done) return;
|
|
150
|
+
done = true;
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
client.destroy();
|
|
153
|
+
resolve(value);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
client.once('connect', () => {
|
|
157
|
+
client.write('{"type":"ping"}\n');
|
|
158
|
+
});
|
|
159
|
+
client.once('data', (buf) => {
|
|
160
|
+
const msg = buf.toString('utf8');
|
|
161
|
+
finish(msg.includes('pong'));
|
|
162
|
+
});
|
|
163
|
+
client.once('error', () => finish(false));
|
|
164
|
+
client.once('close', () => finish(false));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return { viable: connectOk, evidence: connectOk ? 'socket-connect-ping-ok' : 'socket-connect-failed' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeStatus(status: number): CheckStatus {
|
|
171
|
+
if (status >= 500) return 'fail';
|
|
172
|
+
if (status >= 400) return 'warn';
|
|
173
|
+
return 'pass';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function evaluateCredentialHealthSeverity(summary: CredentialHealthSummary): {
|
|
177
|
+
status: CheckStatus;
|
|
178
|
+
severity: CheckSeverity;
|
|
179
|
+
code: string;
|
|
180
|
+
finding: string;
|
|
181
|
+
remediation: string;
|
|
182
|
+
evidence: string;
|
|
183
|
+
} {
|
|
184
|
+
if (summary.breached > 0) {
|
|
185
|
+
return {
|
|
186
|
+
status: 'fail',
|
|
187
|
+
severity: 'high',
|
|
188
|
+
code: 'AURA_DOCTOR_CREDENTIAL_HEALTH_BREACHED',
|
|
189
|
+
finding: 'Credential health check found breached credentials.',
|
|
190
|
+
remediation: 'Rotate breached credentials immediately and rerun scan',
|
|
191
|
+
evidence: `breached=${summary.breached},weak=${summary.weak},reused=${summary.reused},unknown=${summary.unknown}`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (summary.weak > 0 || summary.reused > 0 || summary.unknown > 0) {
|
|
196
|
+
const unknownRemediation = summary.unknown > 0
|
|
197
|
+
? ' Retry health scan with HEALTH_BREACH_CHECK=true to resolve unknown breach status.'
|
|
198
|
+
: '';
|
|
199
|
+
return {
|
|
200
|
+
status: 'warn',
|
|
201
|
+
severity: 'medium',
|
|
202
|
+
code: summary.unknown > 0
|
|
203
|
+
? 'AURA_DOCTOR_CREDENTIAL_HEALTH_WARN_UNKNOWN'
|
|
204
|
+
: 'AURA_DOCTOR_CREDENTIAL_HEALTH_WARN_RISK',
|
|
205
|
+
finding: 'Credential health check found weak/reused/unknown-risk credentials.',
|
|
206
|
+
remediation: `Fix weak/reused credentials and rerun scan.${unknownRemediation}`.trim(),
|
|
207
|
+
evidence: `breached=${summary.breached},weak=${summary.weak},reused=${summary.reused},unknown=${summary.unknown}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
status: 'pass',
|
|
213
|
+
severity: 'info',
|
|
214
|
+
code: 'AURA_DOCTOR_CREDENTIAL_HEALTH_PASS',
|
|
215
|
+
finding: 'Credential health check found no weak/reused/breached/unknown credentials.',
|
|
216
|
+
remediation: 'none',
|
|
217
|
+
evidence: `breached=0,weak=0,reused=0,unknown=0`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function fetchWithStatus(url: string, init?: RequestInit): Promise<{ ok: boolean; status: number; text: string }> {
|
|
222
|
+
const res = await withTimeout(fetch(url, init), HTTP_TIMEOUT_MS, 'http');
|
|
223
|
+
const text = await res.text();
|
|
224
|
+
return { ok: res.ok, status: res.status, text };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function findAuraFile(): string | null {
|
|
228
|
+
let dir = process.cwd();
|
|
229
|
+
while (true) {
|
|
230
|
+
const candidate = path.join(dir, '.aura');
|
|
231
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
232
|
+
const parent = path.dirname(dir);
|
|
233
|
+
if (parent === dir) return null;
|
|
234
|
+
dir = parent;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function listLikelySecretFiles(cwd: string): { checked: number; matches: string[]; timedOut: boolean } {
|
|
239
|
+
const started = Date.now();
|
|
240
|
+
const queue: Array<{ dir: string; depth: number }> = [{ dir: cwd, depth: 0 }];
|
|
241
|
+
let checked = 0;
|
|
242
|
+
const matches: string[] = [];
|
|
243
|
+
|
|
244
|
+
while (queue.length > 0) {
|
|
245
|
+
if (Date.now() - started > SECURITY_TIME_BUDGET_MS) {
|
|
246
|
+
return { checked, matches, timedOut: true };
|
|
247
|
+
}
|
|
248
|
+
const { dir, depth } = queue.shift()!;
|
|
249
|
+
|
|
250
|
+
let entries: fs.Dirent[];
|
|
251
|
+
try {
|
|
252
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
253
|
+
} catch {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const entry of entries) {
|
|
258
|
+
checked += 1;
|
|
259
|
+
if (checked > MAX_SECURITY_ENTRIES) {
|
|
260
|
+
return { checked, matches, timedOut: false };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const full = path.join(dir, entry.name);
|
|
264
|
+
if (entry.isDirectory() && depth < 2 && entry.name !== 'node_modules' && !entry.name.startsWith('.git')) {
|
|
265
|
+
queue.push({ dir: full, depth: depth + 1 });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!entry.isFile()) continue;
|
|
270
|
+
if (/\.env($|\.)/i.test(entry.name) || /token/i.test(entry.name)) {
|
|
271
|
+
matches.push(path.relative(cwd, full));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { checked, matches, timedOut: false };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function runDoctor(options: DoctorOptions): Promise<DoctorResult> {
|
|
280
|
+
const checks: DoctorCheck[] = [];
|
|
281
|
+
|
|
282
|
+
const addCheck = (check: DoctorCheck) => checks.push(check);
|
|
283
|
+
|
|
284
|
+
// runtime.api.health
|
|
285
|
+
let apiHealthy = false;
|
|
286
|
+
try {
|
|
287
|
+
const health = await fetchWithStatus(`${serverUrl()}/health`);
|
|
288
|
+
apiHealthy = health.ok;
|
|
289
|
+
addCheck({
|
|
290
|
+
id: 'runtime.api.health',
|
|
291
|
+
code: health.ok ? 'AURA_DOCTOR_RUNTIME_API_HEALTHY' : 'AURA_DOCTOR_RUNTIME_API_UNREACHABLE',
|
|
292
|
+
severity: health.ok ? 'info' : 'critical',
|
|
293
|
+
status: health.ok ? 'pass' : 'fail',
|
|
294
|
+
finding: health.ok ? 'Aura API is reachable.' : 'Aura API is unreachable.',
|
|
295
|
+
evidence: health.ok ? `health-status-${health.status}` : `health-status-${health.status}`,
|
|
296
|
+
remediation: health.ok ? 'none' : 'Run: aura start',
|
|
297
|
+
});
|
|
298
|
+
} catch (err) {
|
|
299
|
+
addCheck({
|
|
300
|
+
id: 'runtime.api.health',
|
|
301
|
+
code: 'AURA_DOCTOR_RUNTIME_API_UNREACHABLE',
|
|
302
|
+
severity: 'critical',
|
|
303
|
+
status: 'fail',
|
|
304
|
+
finding: 'Aura API is unreachable.',
|
|
305
|
+
evidence: `health-error-${getErrorMessage(err)}`,
|
|
306
|
+
remediation: 'Run: aura start',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// runtime.api.setup + vault checks
|
|
311
|
+
let setup: SetupStatus | null = null;
|
|
312
|
+
try {
|
|
313
|
+
setup = await fetchJson<SetupStatus>('/setup');
|
|
314
|
+
addCheck({
|
|
315
|
+
id: 'runtime.api.setup',
|
|
316
|
+
code: 'AURA_DOCTOR_RUNTIME_SETUP_OK',
|
|
317
|
+
severity: 'info',
|
|
318
|
+
status: 'pass',
|
|
319
|
+
finding: 'Setup endpoint responded.',
|
|
320
|
+
evidence: 'setup-response-valid',
|
|
321
|
+
remediation: 'none',
|
|
322
|
+
});
|
|
323
|
+
} catch (err) {
|
|
324
|
+
addCheck({
|
|
325
|
+
id: 'runtime.api.setup',
|
|
326
|
+
code: 'AURA_DOCTOR_RUNTIME_SETUP_ERROR',
|
|
327
|
+
severity: 'high',
|
|
328
|
+
status: 'fail',
|
|
329
|
+
finding: 'Setup endpoint check failed.',
|
|
330
|
+
evidence: `setup-error-${getErrorMessage(err)}`,
|
|
331
|
+
remediation: 'Run: aura start',
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// runtime.dashboard.reachability (never fail)
|
|
336
|
+
if (!apiHealthy) {
|
|
337
|
+
addCheck({
|
|
338
|
+
id: 'runtime.dashboard.reachability',
|
|
339
|
+
code: 'AURA_DOCTOR_DASHBOARD_SKIPPED_API_UNHEALTHY',
|
|
340
|
+
severity: 'low',
|
|
341
|
+
status: 'warn',
|
|
342
|
+
finding: 'Dashboard check skipped while API is unhealthy.',
|
|
343
|
+
evidence: 'api-unhealthy-dashboard-check-skipped',
|
|
344
|
+
remediation: 'Run: aura start',
|
|
345
|
+
});
|
|
346
|
+
} else {
|
|
347
|
+
try {
|
|
348
|
+
const dashboard = await withTimeout(fetch('http://localhost:4747/app'), 1500, 'dashboard');
|
|
349
|
+
const reachable = dashboard.ok || (dashboard.status >= 300 && dashboard.status < 400);
|
|
350
|
+
addCheck({
|
|
351
|
+
id: 'runtime.dashboard.reachability',
|
|
352
|
+
code: reachable ? 'AURA_DOCTOR_DASHBOARD_REACHABLE' : 'AURA_DOCTOR_DASHBOARD_UNREACHABLE',
|
|
353
|
+
severity: reachable ? 'info' : 'low',
|
|
354
|
+
status: reachable ? 'pass' : 'warn',
|
|
355
|
+
finding: reachable ? 'Dashboard endpoint is reachable.' : 'Dashboard endpoint is not reachable in current mode.',
|
|
356
|
+
evidence: reachable ? 'dashboard-reachable' : 'headless-or-dashboard-not-running',
|
|
357
|
+
remediation: reachable ? 'none' : 'If UI needed: aura start',
|
|
358
|
+
});
|
|
359
|
+
} catch {
|
|
360
|
+
addCheck({
|
|
361
|
+
id: 'runtime.dashboard.reachability',
|
|
362
|
+
code: 'AURA_DOCTOR_DASHBOARD_UNREACHABLE',
|
|
363
|
+
severity: 'low',
|
|
364
|
+
status: 'warn',
|
|
365
|
+
finding: 'Dashboard endpoint is not reachable in current mode.',
|
|
366
|
+
evidence: 'headless-or-dashboard-not-running',
|
|
367
|
+
remediation: 'If UI needed: aura start',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const hasWallet = !!setup?.hasWallet;
|
|
373
|
+
const unlocked = !!setup?.unlocked;
|
|
374
|
+
const hasAddress = !!setup?.address;
|
|
375
|
+
|
|
376
|
+
// vault.exists
|
|
377
|
+
addCheck({
|
|
378
|
+
id: 'vault.exists',
|
|
379
|
+
code: hasWallet ? 'AURA_DOCTOR_VAULT_EXISTS' : 'AURA_DOCTOR_VAULT_MISSING',
|
|
380
|
+
severity: hasWallet ? 'info' : 'high',
|
|
381
|
+
status: hasWallet ? 'pass' : 'fail',
|
|
382
|
+
finding: hasWallet ? 'Primary vault exists.' : 'No vault found.',
|
|
383
|
+
evidence: hasWallet ? 'hasWallet=true' : 'hasWallet=false',
|
|
384
|
+
remediation: hasWallet ? 'none' : 'Run: aura setup',
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// vault.unlock_state
|
|
388
|
+
addCheck({
|
|
389
|
+
id: 'vault.unlock_state',
|
|
390
|
+
code: unlocked ? 'AURA_DOCTOR_VAULT_UNLOCKED' : 'AURA_DOCTOR_VAULT_LOCKED',
|
|
391
|
+
severity: unlocked ? 'info' : 'medium',
|
|
392
|
+
status: unlocked ? 'pass' : 'warn',
|
|
393
|
+
finding: unlocked ? 'Vault is unlocked.' : 'Vault is locked.',
|
|
394
|
+
evidence: `unlocked=${unlocked}`,
|
|
395
|
+
remediation: unlocked ? 'none' : 'Run: aura unlock',
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// vault.primary.address tuple
|
|
399
|
+
let addressStatus: CheckStatus = 'pass';
|
|
400
|
+
let addressSeverity: CheckSeverity = 'info';
|
|
401
|
+
let addressEvidence = 'wallet-locked-address-not-required';
|
|
402
|
+
let addressCode = 'AURA_DOCTOR_VAULT_PRIMARY_ADDRESS_NA_LOCKED';
|
|
403
|
+
let addressFinding = 'Primary address is not required while locked.';
|
|
404
|
+
let addressRemediation = 'none';
|
|
405
|
+
|
|
406
|
+
if (!hasWallet) {
|
|
407
|
+
addressStatus = 'fail';
|
|
408
|
+
addressSeverity = 'high';
|
|
409
|
+
addressEvidence = 'no-wallet';
|
|
410
|
+
addressCode = 'AURA_DOCTOR_VAULT_NO_WALLET';
|
|
411
|
+
addressFinding = 'Primary address unavailable because no vault exists.';
|
|
412
|
+
addressRemediation = 'Run: aura setup';
|
|
413
|
+
} else if (unlocked && hasAddress) {
|
|
414
|
+
addressStatus = 'pass';
|
|
415
|
+
addressSeverity = 'info';
|
|
416
|
+
addressEvidence = 'primary-address-present';
|
|
417
|
+
addressCode = 'AURA_DOCTOR_VAULT_PRIMARY_ADDRESS_PRESENT';
|
|
418
|
+
addressFinding = 'Primary address is present.';
|
|
419
|
+
addressRemediation = 'none';
|
|
420
|
+
} else if (unlocked && !hasAddress) {
|
|
421
|
+
addressStatus = 'warn';
|
|
422
|
+
addressSeverity = 'medium';
|
|
423
|
+
addressEvidence = 'unlocked-without-primary-address';
|
|
424
|
+
addressCode = 'AURA_DOCTOR_VAULT_PRIMARY_ADDRESS_MISSING';
|
|
425
|
+
addressFinding = 'Vault is unlocked but primary address is missing.';
|
|
426
|
+
addressRemediation = 'Run: aura setup --repair';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
addCheck({
|
|
430
|
+
id: 'vault.primary.address',
|
|
431
|
+
code: addressCode,
|
|
432
|
+
severity: addressSeverity,
|
|
433
|
+
status: addressStatus,
|
|
434
|
+
finding: addressFinding,
|
|
435
|
+
evidence: addressEvidence,
|
|
436
|
+
remediation: addressRemediation,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// auth.socket.path
|
|
440
|
+
const socketProbe = await probeSocketViability();
|
|
441
|
+
addCheck({
|
|
442
|
+
id: 'auth.socket.path',
|
|
443
|
+
code: socketProbe.viable ? 'AURA_DOCTOR_AUTH_SOCKET_VIABLE' : 'AURA_DOCTOR_AUTH_SOCKET_NOT_VIABLE',
|
|
444
|
+
severity: socketProbe.viable ? 'info' : 'low',
|
|
445
|
+
status: socketProbe.viable ? 'pass' : 'warn',
|
|
446
|
+
finding: socketProbe.viable ? 'Unix socket auth is viable.' : 'Unix socket auth is not viable.',
|
|
447
|
+
evidence: socketProbe.evidence,
|
|
448
|
+
remediation: socketProbe.viable ? 'none' : 'Run: aura start',
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// auth.token.env
|
|
452
|
+
const envToken = process.env.AURA_TOKEN;
|
|
453
|
+
const tokenPresent = !!envToken;
|
|
454
|
+
const tokenShapeValid = checkTokenShape(envToken);
|
|
455
|
+
addCheck({
|
|
456
|
+
id: 'auth.token.env',
|
|
457
|
+
code: tokenPresent ? (tokenShapeValid ? 'AURA_DOCTOR_AUTH_TOKEN_PRESENT' : 'AURA_DOCTOR_AUTH_TOKEN_INVALID_SHAPE') : 'AURA_DOCTOR_AUTH_TOKEN_MISSING',
|
|
458
|
+
severity: tokenPresent ? (tokenShapeValid ? 'info' : 'medium') : 'low',
|
|
459
|
+
status: tokenPresent ? (tokenShapeValid ? 'pass' : 'warn') : 'warn',
|
|
460
|
+
finding: tokenPresent ? (tokenShapeValid ? 'AURA_TOKEN is present.' : 'AURA_TOKEN is present but malformed.') : 'AURA_TOKEN is not set.',
|
|
461
|
+
evidence: maskToken(envToken),
|
|
462
|
+
remediation: tokenPresent ? (tokenShapeValid ? 'none' : 'Export a valid AURA_TOKEN') : 'Export AURA_TOKEN or rely on socket auth',
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// auth.mode.viability matrix
|
|
466
|
+
const tokenViable = tokenPresent && tokenShapeValid;
|
|
467
|
+
const authViable = socketProbe.viable || tokenViable;
|
|
468
|
+
addCheck({
|
|
469
|
+
id: 'auth.mode.viability',
|
|
470
|
+
code: authViable ? 'AURA_DOCTOR_AUTH_MODE_VIABLE' : 'AURA_DOCTOR_AUTH_MODE_UNAVAILABLE',
|
|
471
|
+
severity: authViable ? 'info' : 'critical',
|
|
472
|
+
status: authViable ? 'pass' : 'fail',
|
|
473
|
+
finding: authViable ? 'At least one auth bootstrap mode is viable.' : 'No viable auth bootstrap mode found.',
|
|
474
|
+
evidence: authViable
|
|
475
|
+
? (socketProbe.viable && tokenViable ? 'socket-and-token-available' : socketProbe.viable ? 'socket-available' : 'token-available')
|
|
476
|
+
: 'no-viable-auth-bootstrap',
|
|
477
|
+
remediation: authViable ? 'none' : 'Start daemon for socket (aura start) or export AURA_TOKEN',
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const authProbeState: AuthProbeState = {
|
|
481
|
+
socketViable: socketProbe.viable,
|
|
482
|
+
tokenPresent,
|
|
483
|
+
tokenShapeValid,
|
|
484
|
+
tokenMask: maskToken(envToken),
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// token introspection (only if env token present + shape valid)
|
|
488
|
+
if (tokenViable) {
|
|
489
|
+
try {
|
|
490
|
+
const validate = await fetchJson<TokenValidateResponse>('/auth/validate', {
|
|
491
|
+
body: { token: envToken },
|
|
492
|
+
});
|
|
493
|
+
authProbeState.tokenValidate = validate;
|
|
494
|
+
} catch (err) {
|
|
495
|
+
authProbeState.tokenValidate = {
|
|
496
|
+
valid: false,
|
|
497
|
+
error: getErrorMessage(err),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// credential.list.readiness (read-only)
|
|
503
|
+
if (!tokenViable) {
|
|
504
|
+
addCheck({
|
|
505
|
+
id: 'credential.list.readiness',
|
|
506
|
+
code: 'AURA_DOCTOR_CREDENTIAL_LIST_TOKEN_UNAVAILABLE',
|
|
507
|
+
severity: 'medium',
|
|
508
|
+
status: 'warn',
|
|
509
|
+
finding: 'Credential list readiness requires explicit token audit mode.',
|
|
510
|
+
evidence: 'socket-mode-no-explicit-token',
|
|
511
|
+
remediation: 'Export AURA_TOKEN for explicit credential-read checks',
|
|
512
|
+
});
|
|
513
|
+
} else {
|
|
514
|
+
try {
|
|
515
|
+
const response = await fetchWithStatus(`${serverUrl()}/credentials`, {
|
|
516
|
+
headers: {
|
|
517
|
+
Authorization: `Bearer ${envToken}`,
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const status = normalizeStatus(response.status);
|
|
522
|
+
const code = response.ok
|
|
523
|
+
? 'AURA_DOCTOR_CREDENTIAL_LIST_READY'
|
|
524
|
+
: response.status === 401 || response.status === 403
|
|
525
|
+
? 'AURA_DOCTOR_CREDENTIAL_LIST_UNAUTHORIZED'
|
|
526
|
+
: 'AURA_DOCTOR_CREDENTIAL_LIST_ERROR';
|
|
527
|
+
|
|
528
|
+
addCheck({
|
|
529
|
+
id: 'credential.list.readiness',
|
|
530
|
+
code,
|
|
531
|
+
severity: status === 'pass' ? 'info' : status === 'warn' ? 'medium' : 'high',
|
|
532
|
+
status,
|
|
533
|
+
finding: response.ok ? 'Credential list endpoint is readable.' : 'Credential list endpoint is not readable.',
|
|
534
|
+
evidence: `credentials-list-http-${response.status}`,
|
|
535
|
+
remediation: response.ok ? 'none' : 'Grant secret:read scope or refresh token',
|
|
536
|
+
});
|
|
537
|
+
} catch (err) {
|
|
538
|
+
addCheck({
|
|
539
|
+
id: 'credential.list.readiness',
|
|
540
|
+
code: 'AURA_DOCTOR_CREDENTIAL_LIST_ERROR',
|
|
541
|
+
severity: 'high',
|
|
542
|
+
status: 'fail',
|
|
543
|
+
finding: 'Credential list endpoint check failed.',
|
|
544
|
+
evidence: `credentials-list-error-${getErrorMessage(err)}`,
|
|
545
|
+
remediation: 'Verify Aura API health and token scope',
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// credential.scope.sanity
|
|
551
|
+
if (!tokenViable) {
|
|
552
|
+
addCheck({
|
|
553
|
+
id: 'credential.scope.sanity',
|
|
554
|
+
code: 'AURA_DOCTOR_SCOPE_TOKEN_INTROSPECTION_UNAVAILABLE',
|
|
555
|
+
severity: 'low',
|
|
556
|
+
status: 'warn',
|
|
557
|
+
finding: 'Token scope introspection unavailable without explicit token.',
|
|
558
|
+
evidence: 'socket-mode-no-explicit-token',
|
|
559
|
+
remediation: 'Export AURA_TOKEN for explicit scope audit',
|
|
560
|
+
});
|
|
561
|
+
} else if (!authProbeState.tokenValidate?.valid) {
|
|
562
|
+
addCheck({
|
|
563
|
+
id: 'credential.scope.sanity',
|
|
564
|
+
code: 'AURA_DOCTOR_SCOPE_TOKEN_INVALID',
|
|
565
|
+
severity: 'high',
|
|
566
|
+
status: 'fail',
|
|
567
|
+
finding: 'Provided token failed validation.',
|
|
568
|
+
evidence: 'token-validation-failed',
|
|
569
|
+
remediation: 'Export a valid AURA_TOKEN with secret:read permissions',
|
|
570
|
+
});
|
|
571
|
+
} else {
|
|
572
|
+
const permissions = authProbeState.tokenValidate.payload?.permissions || [];
|
|
573
|
+
const hasSecretRead = permissions.includes('admin:*') || permissions.includes('secret:read');
|
|
574
|
+
addCheck({
|
|
575
|
+
id: 'credential.scope.sanity',
|
|
576
|
+
code: hasSecretRead ? 'AURA_DOCTOR_SCOPE_OK' : 'AURA_DOCTOR_SCOPE_MISSING_SECRET_READ',
|
|
577
|
+
severity: hasSecretRead ? 'info' : 'high',
|
|
578
|
+
status: hasSecretRead ? 'pass' : 'fail',
|
|
579
|
+
finding: hasSecretRead ? 'Token scope includes secret read access.' : 'Token scope is missing secret:read access.',
|
|
580
|
+
evidence: hasSecretRead ? 'required-scope-present' : 'required-scope-missing-secret-read',
|
|
581
|
+
remediation: hasSecretRead ? 'none' : 'Issue token with secret:read (or admin:*) permission',
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// credential.health.summary
|
|
586
|
+
if (!tokenViable) {
|
|
587
|
+
addCheck({
|
|
588
|
+
id: 'credential.health.summary',
|
|
589
|
+
code: 'AURA_DOCTOR_CREDENTIAL_HEALTH_TOKEN_UNAVAILABLE',
|
|
590
|
+
severity: 'low',
|
|
591
|
+
status: 'warn',
|
|
592
|
+
finding: 'Credential health summary requires explicit token mode.',
|
|
593
|
+
evidence: 'socket-mode-no-explicit-token',
|
|
594
|
+
remediation: 'Export AURA_TOKEN for credential health summary check',
|
|
595
|
+
});
|
|
596
|
+
} else {
|
|
597
|
+
try {
|
|
598
|
+
const response = await fetchWithStatus(`${serverUrl()}/credentials/health/summary`, {
|
|
599
|
+
headers: { Authorization: `Bearer ${envToken}` },
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
if (!response.ok) {
|
|
603
|
+
addCheck({
|
|
604
|
+
id: 'credential.health.summary',
|
|
605
|
+
code: response.status === 401 || response.status === 403
|
|
606
|
+
? 'AURA_DOCTOR_CREDENTIAL_HEALTH_UNAUTHORIZED'
|
|
607
|
+
: 'AURA_DOCTOR_CREDENTIAL_HEALTH_ERROR',
|
|
608
|
+
severity: response.status === 401 || response.status === 403 ? 'medium' : 'high',
|
|
609
|
+
status: normalizeStatus(response.status),
|
|
610
|
+
finding: 'Credential health summary endpoint is not readable.',
|
|
611
|
+
evidence: `credential-health-summary-http-${response.status}`,
|
|
612
|
+
remediation: 'Grant secret:read scope or refresh token',
|
|
613
|
+
});
|
|
614
|
+
} else {
|
|
615
|
+
const data = JSON.parse(response.text) as { summary?: CredentialHealthSummary };
|
|
616
|
+
if (!data.summary) {
|
|
617
|
+
addCheck({
|
|
618
|
+
id: 'credential.health.summary',
|
|
619
|
+
code: 'AURA_DOCTOR_CREDENTIAL_HEALTH_MALFORMED',
|
|
620
|
+
severity: 'high',
|
|
621
|
+
status: 'fail',
|
|
622
|
+
finding: 'Credential health summary payload is malformed.',
|
|
623
|
+
evidence: 'summary-missing',
|
|
624
|
+
remediation: 'Upgrade Aura server and rerun doctor',
|
|
625
|
+
});
|
|
626
|
+
} else {
|
|
627
|
+
const evaluated = evaluateCredentialHealthSeverity(data.summary);
|
|
628
|
+
addCheck({
|
|
629
|
+
id: 'credential.health.summary',
|
|
630
|
+
code: evaluated.code,
|
|
631
|
+
severity: evaluated.severity,
|
|
632
|
+
status: evaluated.status,
|
|
633
|
+
finding: evaluated.finding,
|
|
634
|
+
evidence: evaluated.evidence,
|
|
635
|
+
remediation: evaluated.remediation,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} catch (err) {
|
|
640
|
+
addCheck({
|
|
641
|
+
id: 'credential.health.summary',
|
|
642
|
+
code: 'AURA_DOCTOR_CREDENTIAL_HEALTH_ERROR',
|
|
643
|
+
severity: 'high',
|
|
644
|
+
status: 'fail',
|
|
645
|
+
finding: 'Credential health summary check failed.',
|
|
646
|
+
evidence: `credential-health-summary-error-${getErrorMessage(err)}`,
|
|
647
|
+
remediation: 'Verify Aura API health and token scope',
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// .aura checks
|
|
653
|
+
const auraFile = findAuraFile();
|
|
654
|
+
if (!auraFile) {
|
|
655
|
+
addCheck({
|
|
656
|
+
id: 'aura_file.discovery',
|
|
657
|
+
code: 'AURA_DOCTOR_AURA_FILE_MISSING',
|
|
658
|
+
severity: 'low',
|
|
659
|
+
status: 'warn',
|
|
660
|
+
finding: 'No .aura file found in current/parent directories.',
|
|
661
|
+
evidence: 'aura-file-not-found',
|
|
662
|
+
remediation: 'Run: aura env init',
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
addCheck({
|
|
666
|
+
id: 'aura_file.parse',
|
|
667
|
+
code: 'AURA_DOCTOR_AURA_FILE_PARSE_SKIPPED',
|
|
668
|
+
severity: 'info',
|
|
669
|
+
status: 'pass',
|
|
670
|
+
finding: '.aura parse check skipped.',
|
|
671
|
+
evidence: 'no-aura-file',
|
|
672
|
+
remediation: 'none',
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
addCheck({
|
|
676
|
+
id: 'aura_file.mapping_resolution',
|
|
677
|
+
code: 'AURA_DOCTOR_AURA_MAPPING_SKIPPED',
|
|
678
|
+
severity: 'info',
|
|
679
|
+
status: 'pass',
|
|
680
|
+
finding: '.aura mapping resolution skipped.',
|
|
681
|
+
evidence: 'no-aura-file',
|
|
682
|
+
remediation: 'none',
|
|
683
|
+
});
|
|
684
|
+
} else {
|
|
685
|
+
addCheck({
|
|
686
|
+
id: 'aura_file.discovery',
|
|
687
|
+
code: 'AURA_DOCTOR_AURA_FILE_FOUND',
|
|
688
|
+
severity: 'info',
|
|
689
|
+
status: 'pass',
|
|
690
|
+
finding: '.aura file discovered.',
|
|
691
|
+
evidence: path.relative(process.cwd(), auraFile) || '.aura',
|
|
692
|
+
remediation: 'none',
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
let mappings: AuraMapping[] = [];
|
|
696
|
+
let parseOk = false;
|
|
697
|
+
try {
|
|
698
|
+
mappings = parseAuraFile(auraFile);
|
|
699
|
+
parseOk = true;
|
|
700
|
+
addCheck({
|
|
701
|
+
id: 'aura_file.parse',
|
|
702
|
+
code: 'AURA_DOCTOR_AURA_FILE_PARSE_OK',
|
|
703
|
+
severity: 'info',
|
|
704
|
+
status: 'pass',
|
|
705
|
+
finding: '.aura file parsed successfully.',
|
|
706
|
+
evidence: `mappings=${mappings.length}`,
|
|
707
|
+
remediation: 'none',
|
|
708
|
+
});
|
|
709
|
+
} catch (err) {
|
|
710
|
+
addCheck({
|
|
711
|
+
id: 'aura_file.parse',
|
|
712
|
+
code: 'AURA_DOCTOR_AURA_FILE_PARSE_FAILED',
|
|
713
|
+
severity: 'high',
|
|
714
|
+
status: 'fail',
|
|
715
|
+
finding: '.aura file parse failed.',
|
|
716
|
+
evidence: getErrorMessage(err),
|
|
717
|
+
remediation: 'Fix .aura syntax and rerun aura doctor',
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (!parseOk) {
|
|
722
|
+
addCheck({
|
|
723
|
+
id: 'aura_file.mapping_resolution',
|
|
724
|
+
code: 'AURA_DOCTOR_AURA_MAPPING_SKIPPED_PARSE_FAIL',
|
|
725
|
+
severity: 'info',
|
|
726
|
+
status: 'pass',
|
|
727
|
+
finding: '.aura mapping resolution skipped due to parse failure.',
|
|
728
|
+
evidence: 'parse-failed',
|
|
729
|
+
remediation: 'none',
|
|
730
|
+
});
|
|
731
|
+
} else if (!tokenViable) {
|
|
732
|
+
addCheck({
|
|
733
|
+
id: 'aura_file.mapping_resolution',
|
|
734
|
+
code: 'AURA_DOCTOR_AURA_MAPPING_TOKEN_UNAVAILABLE',
|
|
735
|
+
severity: 'medium',
|
|
736
|
+
status: 'warn',
|
|
737
|
+
finding: '.aura mapping resolution requires explicit token mode.',
|
|
738
|
+
evidence: 'socket-mode-no-explicit-token',
|
|
739
|
+
remediation: 'Export AURA_TOKEN for mapping resolution audit',
|
|
740
|
+
});
|
|
741
|
+
} else {
|
|
742
|
+
const cappedMappings = mappings.slice(0, MAX_AURA_MAPPINGS);
|
|
743
|
+
const uniqueCredentialNames = [...new Set(cappedMappings.map((m) => m.credentialName))].slice(0, MAX_UNIQUE_CREDENTIAL_PROBES);
|
|
744
|
+
const credIdByName = new Map<string, string>();
|
|
745
|
+
const failures = new Set<string>();
|
|
746
|
+
|
|
747
|
+
for (const credName of uniqueCredentialNames) {
|
|
748
|
+
try {
|
|
749
|
+
const resp = await fetchWithStatus(`${serverUrl()}/credentials?q=${encodeURIComponent(credName)}`, {
|
|
750
|
+
headers: { Authorization: `Bearer ${envToken}` },
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
if (!resp.ok) {
|
|
754
|
+
failures.add(`lookup:${credName}`);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const data = JSON.parse(resp.text) as { credentials?: Array<{ id: string; name: string }> };
|
|
759
|
+
const list = data.credentials || [];
|
|
760
|
+
const exact = list.find((c) => c.name.toLowerCase() === credName.toLowerCase()) || list[0];
|
|
761
|
+
if (!exact) failures.add(`missing-credential:${credName}`);
|
|
762
|
+
else credIdByName.set(credName, exact.id);
|
|
763
|
+
} catch {
|
|
764
|
+
failures.add(`lookup:${credName}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
for (const mapping of cappedMappings) {
|
|
769
|
+
const id = credIdByName.get(mapping.credentialName);
|
|
770
|
+
if (!id) {
|
|
771
|
+
failures.add(`${mapping.envVar}->${mapping.credentialName}.${mapping.field}`);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
try {
|
|
776
|
+
const resp = await fetchWithStatus(`${serverUrl()}/credentials/${id}/read`, {
|
|
777
|
+
method: 'POST',
|
|
778
|
+
headers: { Authorization: `Bearer ${envToken}` },
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
if (!resp.ok) {
|
|
782
|
+
failures.add(`${mapping.envVar}->${mapping.credentialName}.${mapping.field}`);
|
|
783
|
+
}
|
|
784
|
+
} catch {
|
|
785
|
+
failures.add(`${mapping.envVar}->${mapping.credentialName}.${mapping.field}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const failureList = [...failures];
|
|
790
|
+
const limitedEvidence = failureList.slice(0, 10);
|
|
791
|
+
const more = failureList.length > 10 ? ` (+${failureList.length - 10} more)` : '';
|
|
792
|
+
|
|
793
|
+
addCheck({
|
|
794
|
+
id: 'aura_file.mapping_resolution',
|
|
795
|
+
code: failureList.length === 0 ? 'AURA_DOCTOR_AURA_MAPPING_OK' : 'AURA_DOCTOR_AURA_MAPPING_FAILED',
|
|
796
|
+
severity: failureList.length === 0 ? 'info' : 'high',
|
|
797
|
+
status: failureList.length === 0 ? 'pass' : 'fail',
|
|
798
|
+
finding: failureList.length === 0 ? '.aura mappings are resolvable.' : 'One or more .aura mappings are not resolvable.',
|
|
799
|
+
evidence: failureList.length === 0 ? `checked=${cappedMappings.length}` : `${limitedEvidence.join(', ')}${more}`,
|
|
800
|
+
remediation: failureList.length === 0 ? 'none' : 'Fix missing credentials/fields or token scope',
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// MCP checks (read-only)
|
|
806
|
+
const mcpHelp = spawnSync('npx', ['aurawallet', 'mcp', '--help'], {
|
|
807
|
+
encoding: 'utf8',
|
|
808
|
+
timeout: 1500,
|
|
809
|
+
});
|
|
810
|
+
const mcpAvailable = !mcpHelp.error && mcpHelp.status === 0;
|
|
811
|
+
addCheck({
|
|
812
|
+
id: 'mcp.command.available',
|
|
813
|
+
code: mcpAvailable ? 'AURA_DOCTOR_MCP_COMMAND_AVAILABLE' : 'AURA_DOCTOR_MCP_COMMAND_UNAVAILABLE',
|
|
814
|
+
severity: mcpAvailable ? 'info' : 'medium',
|
|
815
|
+
status: mcpAvailable ? 'pass' : 'warn',
|
|
816
|
+
finding: mcpAvailable ? 'MCP command is invokable.' : 'MCP command is not invokable.',
|
|
817
|
+
evidence: mcpAvailable ? 'mcp-help-ok' : `mcp-help-failed-${mcpHelp.status ?? 'error'}`,
|
|
818
|
+
remediation: mcpAvailable ? 'none' : 'Install dependencies and ensure npx aurawallet mcp is available',
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
const mcpConfigs = [
|
|
822
|
+
path.join(process.cwd(), '.vscode', 'mcp.json'),
|
|
823
|
+
path.join(os.homedir(), '.cursor', 'mcp.json'),
|
|
824
|
+
path.join(os.homedir(), '.windsurf', 'mcp.json'),
|
|
825
|
+
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
826
|
+
];
|
|
827
|
+
|
|
828
|
+
const existingConfigs = mcpConfigs.filter((p) => fs.existsSync(p));
|
|
829
|
+
let parseFailures = 0;
|
|
830
|
+
for (const file of existingConfigs) {
|
|
831
|
+
try {
|
|
832
|
+
JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
833
|
+
} catch {
|
|
834
|
+
parseFailures += 1;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
addCheck({
|
|
839
|
+
id: 'mcp.config.footprint',
|
|
840
|
+
code: parseFailures > 0 ? 'AURA_DOCTOR_MCP_CONFIG_INVALID' : existingConfigs.length > 0 ? 'AURA_DOCTOR_MCP_CONFIG_PRESENT' : 'AURA_DOCTOR_MCP_CONFIG_ABSENT',
|
|
841
|
+
severity: parseFailures > 0 ? 'medium' : existingConfigs.length > 0 ? 'info' : 'low',
|
|
842
|
+
status: parseFailures > 0 ? 'warn' : existingConfigs.length > 0 ? 'pass' : 'warn',
|
|
843
|
+
finding: parseFailures > 0
|
|
844
|
+
? 'One or more MCP config files are invalid JSON.'
|
|
845
|
+
: existingConfigs.length > 0
|
|
846
|
+
? 'MCP config footprint detected.'
|
|
847
|
+
: 'No MCP config footprint detected.',
|
|
848
|
+
evidence: parseFailures > 0
|
|
849
|
+
? `invalid-json-count=${parseFailures}`
|
|
850
|
+
: existingConfigs.length > 0
|
|
851
|
+
? `configs=${existingConfigs.length}`
|
|
852
|
+
: 'no-known-mcp-config-files',
|
|
853
|
+
remediation: parseFailures > 0
|
|
854
|
+
? 'Fix malformed MCP config JSON files'
|
|
855
|
+
: existingConfigs.length > 0
|
|
856
|
+
? 'none'
|
|
857
|
+
: 'Run: aura mcp --install',
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
addCheck({
|
|
861
|
+
id: 'mcp.auth.forecast',
|
|
862
|
+
code: authViable ? 'AURA_DOCTOR_MCP_AUTH_FORECAST_READY' : 'AURA_DOCTOR_MCP_AUTH_FORECAST_BLOCKED',
|
|
863
|
+
severity: authViable ? 'info' : 'high',
|
|
864
|
+
status: authViable ? 'pass' : 'fail',
|
|
865
|
+
finding: authViable ? 'MCP auth bootstrap appears ready.' : 'MCP auth bootstrap is blocked.',
|
|
866
|
+
evidence: authViable ? 'auth-bootstrap-viable' : 'no-viable-auth-bootstrap',
|
|
867
|
+
remediation: authViable ? 'none' : 'Start daemon socket or export AURA_TOKEN before MCP use',
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// Extension check
|
|
871
|
+
addCheck({
|
|
872
|
+
id: 'extension.detectability.cli_mode',
|
|
873
|
+
code: 'AURA_DOCTOR_EXTENSION_NOT_DETECTABLE_CLI_MODE',
|
|
874
|
+
severity: 'low',
|
|
875
|
+
status: 'warn',
|
|
876
|
+
finding: 'Extension session state is not detectable in CLI mode.',
|
|
877
|
+
evidence: 'not-detectable-in-this-mode',
|
|
878
|
+
remediation: 'Use extension UI diagnostics for handshake details',
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// Security checks
|
|
882
|
+
if (!tokenViable) {
|
|
883
|
+
addCheck({
|
|
884
|
+
id: 'security.token_scope.breadth',
|
|
885
|
+
code: 'AURA_DOCTOR_SECURITY_TOKEN_INTROSPECTION_UNAVAILABLE',
|
|
886
|
+
severity: 'low',
|
|
887
|
+
status: 'warn',
|
|
888
|
+
finding: 'Cannot evaluate token scope breadth without explicit token.',
|
|
889
|
+
evidence: 'socket-mode-no-explicit-token',
|
|
890
|
+
remediation: 'Export AURA_TOKEN for explicit scope audit',
|
|
891
|
+
});
|
|
892
|
+
} else {
|
|
893
|
+
const permissions = authProbeState.tokenValidate?.payload?.permissions || [];
|
|
894
|
+
const broad = permissions.some((p) => p === 'admin:*' || p === '*' || p.endsWith(':*'));
|
|
895
|
+
addCheck({
|
|
896
|
+
id: 'security.token_scope.breadth',
|
|
897
|
+
code: broad ? 'AURA_DOCTOR_SECURITY_SCOPE_BROAD' : 'AURA_DOCTOR_SECURITY_SCOPE_LEAST_PRIVILEGE',
|
|
898
|
+
severity: broad ? 'medium' : 'info',
|
|
899
|
+
status: broad ? 'warn' : 'pass',
|
|
900
|
+
finding: broad ? 'Token includes broad wildcard/admin scope.' : 'Token scope appears least-privilege oriented.',
|
|
901
|
+
evidence: broad ? 'broad-scope-detected' : 'no-broad-scope-detected',
|
|
902
|
+
remediation: broad ? 'Issue a narrower token for routine automation' : 'none',
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const scan = listLikelySecretFiles(process.cwd());
|
|
907
|
+
addCheck({
|
|
908
|
+
id: 'security.plaintext_token.artifacts',
|
|
909
|
+
code: scan.timedOut
|
|
910
|
+
? 'AURA_DOCTOR_SECURITY_SCAN_TIME_BUDGET_EXCEEDED'
|
|
911
|
+
: scan.matches.length > 0
|
|
912
|
+
? 'AURA_DOCTOR_SECURITY_ARTIFACT_HINTS_FOUND'
|
|
913
|
+
: 'AURA_DOCTOR_SECURITY_ARTIFACT_HINTS_NONE',
|
|
914
|
+
severity: scan.timedOut ? 'low' : scan.matches.length > 0 ? 'medium' : 'info',
|
|
915
|
+
status: scan.timedOut ? 'warn' : scan.matches.length > 0 ? 'warn' : 'pass',
|
|
916
|
+
finding: scan.timedOut
|
|
917
|
+
? 'Security artifact scan hit time budget.'
|
|
918
|
+
: scan.matches.length > 0
|
|
919
|
+
? 'Potential plaintext secret artifacts detected.'
|
|
920
|
+
: 'No obvious plaintext secret artifacts detected.',
|
|
921
|
+
evidence: scan.timedOut
|
|
922
|
+
? `scan-time-budget-exceeded checked=${scan.checked}`
|
|
923
|
+
: scan.matches.length > 0
|
|
924
|
+
? `matches=${scan.matches.slice(0, 10).join(',')}${scan.matches.length > 10 ? ' (+more)' : ''}`
|
|
925
|
+
: `checked=${scan.checked}`,
|
|
926
|
+
remediation: scan.matches.length > 0
|
|
927
|
+
? 'Move secrets to Aura vault and remove plaintext files'
|
|
928
|
+
: scan.timedOut
|
|
929
|
+
? 'Rerun in a smaller working directory for complete scan'
|
|
930
|
+
: 'none',
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
const stalenessHours = process.env.AURA_TOKEN_ISSUED_AT
|
|
934
|
+
? Math.floor((Date.now() - new Date(process.env.AURA_TOKEN_ISSUED_AT).getTime()) / (1000 * 60 * 60))
|
|
935
|
+
: null;
|
|
936
|
+
const stale = stalenessHours !== null && Number.isFinite(stalenessHours) && stalenessHours > 24;
|
|
937
|
+
addCheck({
|
|
938
|
+
id: 'security.auth_artifact.staleness',
|
|
939
|
+
code: stale ? 'AURA_DOCTOR_SECURITY_AUTH_ARTIFACT_STALE' : 'AURA_DOCTOR_SECURITY_AUTH_ARTIFACT_FRESH_OR_UNKNOWN',
|
|
940
|
+
severity: stale ? 'low' : 'info',
|
|
941
|
+
status: stale ? 'warn' : 'pass',
|
|
942
|
+
finding: stale ? 'Auth artifact appears stale.' : 'Auth artifact freshness is acceptable or unavailable.',
|
|
943
|
+
evidence: stale ? `token-age-hours=${stalenessHours}` : 'no-staleness-metadata',
|
|
944
|
+
remediation: stale ? 'Rotate token to reduce exposure window' : 'none',
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const summary = checks.reduce(
|
|
948
|
+
(acc, check) => {
|
|
949
|
+
acc[check.status] += 1;
|
|
950
|
+
return acc;
|
|
951
|
+
},
|
|
952
|
+
{ pass: 0, warn: 0, fail: 0 }
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
const hasBlocking = summary.fail > 0 || (options.strict && summary.warn > 0);
|
|
956
|
+
|
|
957
|
+
return {
|
|
958
|
+
ok: !hasBlocking,
|
|
959
|
+
mode: options.strict ? 'strict' : 'default',
|
|
960
|
+
summary,
|
|
961
|
+
checks,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function formatHuman(checks: DoctorCheck[]): string {
|
|
966
|
+
return checks
|
|
967
|
+
.map((c) => `${checkBadge(c.status)} ${c.id}\n ${c.finding}\n evidence: ${c.evidence}\n remediation: ${c.remediation}`)
|
|
968
|
+
.join('\n');
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export function mapExitCode(result: DoctorResult): number {
|
|
972
|
+
return result.ok ? EXIT.OK : EXIT.FAIL;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async function main() {
|
|
976
|
+
try {
|
|
977
|
+
const options = parseArgs(process.argv.slice(2));
|
|
978
|
+
const result = await runDoctor(options);
|
|
979
|
+
|
|
980
|
+
if (options.json) {
|
|
981
|
+
console.log(JSON.stringify(result, null, 2));
|
|
982
|
+
} else {
|
|
983
|
+
printBanner('DOCTOR');
|
|
984
|
+
console.log(formatHuman(result.checks));
|
|
985
|
+
printSection('Summary');
|
|
986
|
+
console.log(` pass=${result.summary.pass} warn=${result.summary.warn} fail=${result.summary.fail}`);
|
|
987
|
+
if (options.strict) console.log('Mode: strict');
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
process.exit(mapExitCode(result));
|
|
991
|
+
} catch (err) {
|
|
992
|
+
const message = getErrorMessage(err);
|
|
993
|
+
if (message.toLowerCase().startsWith('unknown flag:')) {
|
|
994
|
+
console.error(message);
|
|
995
|
+
process.exit(EXIT.ARGS);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
console.error(`Doctor internal error: ${message}`);
|
|
999
|
+
process.exit(EXIT.INTERNAL);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (require.main === module) {
|
|
1004
|
+
main();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
export { runDoctor, parseArgs };
|