aws-readonly-dashboard
v1.5.0
Published
Local, strictly read-only AWS dashboard for cost, security, infrastructure monitoring (ECS, EC2, RDS, ElastiCache, ALB/NLB), CloudWatch, CloudTrail and Compute Optimizer insights, with optional user-triggered AI analysis.
Maintainers
Readme
aws-readonly-dashboard
A local, strictly read-only AWS dashboard for cost, security, observability and optimisation — with optional, explicitly triggered AI insights.
aws-readonly-dashboard starts a small server on your machine and opens a browser
dashboard that inspects your AWS accounts using your existing AWS credentials. It
shows what you are spending, where your security posture is weak, how the servers
actually running your workloads are behaving, what AWS itself recommends you resize,
which log groups are quietly growing, and what happened in CloudTrail — and it can
hand a sanitized summary of any of that to an AI provider when, and only when, you
ask it to.
It cannot change anything in AWS. That is enforced in the architecture, not in the UI: every AWS call passes through one access layer that refuses any operation outside a compile-time read-only allowlist.
┌──────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ Dashboard│──▶│ AWS Access │──▶│ Read-only │──▶│ API call tracker │──▶ AWS SDK ──▶ AWS
│ / API │ │ Layer │ │ enforcement │ │ │
└──────────┘ └──────────────┘ └─────────────────┘ └──────────────────┘Contents
- Quick start
- Features
- Infrastructure monitoring
- Installation
- Usage
- Progressive loading
- AWS permissions
- The read-only guarantee
- AWS API usage accounting
- What costs money
- Configuration
- AI insights
- Privacy
- Security
- Uninstalling
- Troubleshooting
- Development
- Contributing
- License
Quick start
npx aws-readonly-dashboardAWS Read-Only Dashboard
Starting local server...
Port: 9001
AWS profiles discovered: 4
Gemini CLI: Available
AWS access: read-only (mutating API calls are refused by design)
Dashboard: http://127.0.0.1:9001If port 9001 is busy, the next free port is used automatically — you never have to kill another process to start the dashboard:
Port 9001 is occupied.
Using port 9002.
Dashboard: http://127.0.0.1:9002Features
AWS access
- Strict read-only architecture — a single access layer, three independent enforcement barriers, and a frozen operation allowlist that no setting, file or environment variable can widen.
- AWS profile discovery — AWS CLI profiles, SSO / IAM Identity Center profiles,
assume-role profiles,
credential_processprofiles and environment credentials are all discovered from your local configuration. - Multi-profile support — select several profiles and see each account in its own clearly labelled section. Profile and account context travels with every row.
- Region selection — checkbox selection with Select All / Clear All, persisted locally. Global resources (IAM, the S3 bucket inventory, Cost Explorer, Trusted Advisor) are always handled separately and never mislabelled as regional.
- AWS API-call accounting — a live counter of the AWS calls this tool makes, with a drill-down by category, operation, profile, account, region and the dashboard action that caused each call.
- Cost transparency — the few AWS APIs that are billed are marked in the UI
before you press anything: a
$against the section in the sidebar, a tag beside its title, and a line saying what AWS charges and what this session has spent so far. See What costs money.
Analysis
- Billing / cost — daily, weekly and monthly charts; service and region
breakdowns; rolling period comparison (1/7/14/30/60/90 days, default 7);
configurable dollar and percentage thresholds (default
$20/10%); explicit classification of increases, decreases, new costs and removed costs. - Security — a modular analyzer covering Security Hub, GuardDuty, Inspector, IAM Access Analyzer, Trusted Advisor, AWS Config, CloudTrail configuration, public S3 buckets, Lambda VPC and policy exposure, security groups open to the internet, and IAM account hygiene. Findings carry severity, status, evidence, reasoning and a manual recommendation. Where a check inspects only part of a large inventory, it says so rather than implying full coverage.
- Infrastructure monitoring — ECS services, EC2 instances, RDS databases, ElastiCache Redis/Valkey and Application/Network Load Balancers, with their headline metrics, the events that explain them translated into plain English, and derived signals such as "restarting repeatedly after running out of memory" or "3 of 5 targets are failing health checks". Fetched only when the section is opened. See Infrastructure monitoring.
- Compute Optimizer — EC2, Auto Scaling, EBS, Lambda, ECS, RDS and idle-resource recommendations, showing only the figures AWS itself provides.
- CloudWatch — largest log groups, rapid growth (percentage and absolute thresholds), missing retention and long retention.
- CloudTrail — a dynamic search over time, region, event name, service, IAM identity, resource, source IP, read/write type, outcome and free text, with UI pagination over large result sets.
Dashboard
- Tables that behave like tables — every section's table sorts on any column, searches the values underneath it (not the rendered text), pages at 25/50/100/All, and keeps your sort and page when you switch tabs and come back.
- Progressive loading — sections that take a while (security especially) render their first results as soon as they exist and keep filling in, with a progress bar and an explicit "these results are incomplete" banner, instead of showing a blank spinner until everything has finished.
AI (optional)
- Gemini CLI as the default provider, reusing your existing Gemini authentication. The header reports one of three honest states — Available, Not authenticated, or Non-LLM mode — and never guesses which one applies.
- Custom LLM REST endpoint for teams with an internal gateway — you define the URL, method, headers, body template and response path.
- Never automatic — AI runs only when you press an Analyze button.
- Analyze only what you picked — tick rows in any section and the button says so: Analyze 12 selected, Analyze filtered (87) or Analyze all (431). Only what the button names is sent, and the result repeats the scope so a slice is never read as a verdict on the whole account.
- Batched requests — a large analysis is split into several provider requests, each small enough to finish inside the timeout, with progress, partial results and a Stop button. See AI request batching.
- Data minimisation — only the sections you select, compacted and aggregated.
- PII sanitization and pseudonymization before anything leaves the machine.
- AI history, off by default, storing only sanitized requests and responses.
Infrastructure monitoring
The Infrastructure tab covers the long-lived things that serve traffic: ECS services (Fargate and EC2 launch types), EC2 instances, RDS databases, ElastiCache Redis/Valkey, and Application and Network Load Balancers.
Like Security, it fetches nothing until you open it. The scan streams: each resource type appears as soon as it has been listed, and metrics fill the rows in afterwards.
What it shows
| | Headline metrics | Events and signals | |---|---|---| | ECS services | CPU, memory, desired vs running tasks | OOM kills, crash loops, image-pull failures, placement failures, deployments rolled back by the circuit breaker, tasks failing load balancer health checks, Spot reclaims | | EC2 instances | CPU, status-check failures, network, CPU credits on burstable families | scheduled retirement and reboots, status checks failing on the AWS side or inside the guest, credits exhausted | | RDS | CPU, connections, freeable memory, free storage, read latency | failovers, low storage and storage-full, replication stopped and resumed, maintenance, restarts | | ElastiCache | engine CPU, memory used, evictions, connections | primary failover, node replacement, recovery, scaling | | Load balancers | requests, p95 target response time, target and ELB 5xx, unhealthy targets (flows and TCP resets for NLBs) | target health reasons — wrong status code, timeout, refused connection, draining |
Opening a row fetches that resource's full metric set — the list view
deliberately asks for four or five metrics per resource, because CloudWatch charges
per metric requested. For a database with Performance Insights enabled, the panel
also shows average active sessions, what the database was waiting on
(db.wait_event), and the busiest statements in AWS's tokenized form, with literal
values already removed.
Plain English, with AWS's own words kept
AWS writes events for the machine that emitted them. The dashboard translates the patterns it recognises and always keeps the original underneath an “AWS said” disclosure, so a translation can be checked rather than trusted:
| AWS says | The dashboard says |
|---|---|
| OutOfMemoryError: Container killed due to memory usage | Ran out of memory and was killed — the container used more than its task definition allows (1024 MiB) |
| rolloutState: FAILED — ECS deployment circuit breaker: task failed to start | Deployment failed and was rolled back — the version you intended to ship is not the version running |
| The free storage capacity for DB instance: orders-prod is low at 4% | Running low on disk space — PostgreSQL stops accepting writes when it runs out |
| Target.ResponseCodeMismatch: Health checks failed with these codes: [302] | Health check failing — the target answers with the wrong status code (expected 200) |
| The instance is running on degraded hardware and is scheduled for retirement. | AWS will retire this instance — on 4 Oct, unless you move it first |
An event that matches no translator is shown verbatim and labelled as not translated. Nothing is invented to fill a gap.
What it refuses to claim
- A metric CloudWatch did not return is “not collected”, with the reason where one is known — never a zero. EC2 publishes no memory metric without the CloudWatch agent, ECS publishes no per-task metrics without Container Insights, and both say so.
- A resource whose metrics could not be read is Unknown, never Healthy.
- A resource with a critical or high-severity event in the window is never shown as Healthy, even when no threshold was crossed.
- Serverless ElastiCache caches are listed and labelled; their metric set differs, and the dashboard reports their configuration rather than measuring them with metrics that do not apply.
Event retention, as AWS defines it
RDS and ElastiCache keep 14 days of events, so the configured lookback is capped there. ECS keeps only its most recent service events, and load balancers publish no event history at all — their target health is read live. The section states the window it could actually see.
Installation
Global install:
npm install -g aws-readonly-dashboard
aws-readonly-dashboardOne-off, no install:
npx aws-readonly-dashboardRequires Node.js 18.17 or newer.
Usage
Usage:
aws-readonly-dashboard [options]
Options:
-p, --port <number> Preferred port (default: 9001). If busy, the next free
port is used automatically.
--host <address> Interface to bind (default: 127.0.0.1). Binding to a
non-loopback address exposes the dashboard on your network.
--profile <name> Pre-select an AWS profile for this session.
--region <list> Pre-select regions for this session (comma separated).
--no-open Do not open a browser automatically.
--config-dir <path> Directory for local configuration
(default: ~/.aws-readonly-dashboard).
--log-level <level> debug | info | warn | error | silent (default: info).
-v, --version Print the version and exit.
-h, --help Print help and exit.Examples:
aws-readonly-dashboard
aws-readonly-dashboard --port 8080 --profile prd --region us-east-1,eu-west-1
aws-readonly-dashboard --no-open --log-level debugWorkflow
- Start the dashboard; it opens in your browser.
- Pick one or more profiles in the header. Each selected profile is validated
with a single
sts:GetCallerIdentitycall. - Pick your regions. Global resources are always included separately.
- Open a section. Opening it fetches that section's AWS data; returning to it reuses what is already in memory. Each section has its own Refresh, and the header has Refresh All. Sections that take more than a moment stream partial results as they arrive — see Progressive loading.
- Optionally press an Analyze button to send that section — sanitized — to your configured AI provider.
Progressive loading
A full security scan across several profiles and a dozen regions is hundreds of AWS calls. Waiting for all of them before drawing anything means staring at a spinner long past the ten seconds that is generally accepted as the limit of a user's attention, with no way to tell a slow scan from a stuck one.
Billing, security, CloudWatch and Compute Optimizer therefore load as jobs. The browser starts the job, then polls it, and every poll returns whatever the scan has produced so far:
POST /api/sections/security { "stream": true }
-> { id, section, status: "running", progress, partial }
GET /api/jobs/<id>
-> { status, progress: { completed, total, label }, partial }
DELETE /api/jobs/<id>
-> { cancelled: true }What you see while a section is still working:
- the findings, rows and charts that already exist, rendered normally;
- a progress bar with a real count (
12 / 22) and the name of the check or profile currently running; - a "Still scanning — these results are incomplete" banner, so a partial view is never mistaken for a finished one.
The progress count is a true position, not an estimate: each check reports its own
completion and per-profile units are aggregated rather than summed into a
meaningless ratio. Identical requests are de-duplicated, so asking for the same
section again while a scan is in flight attaches to the running job instead of
starting a second one. DELETE /api/jobs/<id> stops a job at the next unit
boundary; no AWS calls are made after that point.
This is presentation only. It changes nothing about which AWS calls are made, the read-only guarantee, or the API-call accounting — a partial render costs exactly the calls it reports in AWS API Usage.
AWS permissions
The dashboard needs read permissions only. Nothing in the allowlist can modify AWS, and a policy granting write permissions would not enable any new behaviour: the application would still refuse to call those APIs.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AwsReadOnlyDashboard",
"Effect": "Allow",
"Action": [
"access-analyzer:GetFinding",
"access-analyzer:ListAnalyzers",
"access-analyzer:ListFindings",
"access-analyzer:ListFindingsV2",
"ce:GetCostAndUsage",
"ce:GetCostCategories",
"ce:GetDimensionValues",
"ce:GetTags",
"cloudtrail:DescribeTrails",
"cloudtrail:GetEventSelectors",
"cloudtrail:GetTrailStatus",
"cloudtrail:ListTrails",
"cloudtrail:LookupEvents",
"cloudwatch:GetMetricData",
"cloudwatch:GetMetricStatistics",
"cloudwatch:ListMetrics",
"compute-optimizer:GetAutoScalingGroupRecommendations",
"compute-optimizer:GetEBSVolumeRecommendations",
"compute-optimizer:GetEC2InstanceRecommendations",
"compute-optimizer:GetECSServiceRecommendations",
"compute-optimizer:GetEnrollmentStatus",
"compute-optimizer:GetIdleRecommendations",
"compute-optimizer:GetLambdaFunctionRecommendations",
"compute-optimizer:GetRDSDatabaseRecommendations",
"compute-optimizer:GetRecommendationSummaries",
"config:DescribeComplianceByConfigRule",
"config:DescribeConfigRules",
"config:DescribeConfigurationRecorderStatus",
"config:DescribeConfigurationRecorders",
"config:DescribeDeliveryChannels",
"config:GetComplianceDetailsByConfigRule",
"ec2:DescribeInstanceStatus",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstances",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeRegions",
"ec2:DescribeSecurityGroupRules",
"ec2:DescribeSecurityGroups",
"ec2:DescribeVolumes",
"ec2:DescribeVpcs",
"ecs:DescribeClusters",
"ecs:DescribeContainerInstances",
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition",
"ecs:DescribeTasks",
"ecs:ListClusters",
"ecs:ListContainerInstances",
"ecs:ListServices",
"ecs:ListTasks",
"elasticache:DescribeCacheClusters",
"elasticache:DescribeCacheParameters",
"elasticache:DescribeEvents",
"elasticache:DescribeReplicationGroups",
"elasticache:DescribeServerlessCaches",
"elasticloadbalancing:DescribeListeners",
"elasticloadbalancing:DescribeLoadBalancerAttributes",
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetHealth",
"guardduty:GetDetector",
"guardduty:GetFindings",
"guardduty:ListDetectors",
"guardduty:ListFindings",
"iam:GetAccessKeyLastUsed",
"iam:GetAccountPasswordPolicy",
"iam:GetAccountSummary",
"iam:ListAccessKeys",
"iam:ListAttachedUserPolicies",
"iam:ListMFADevices",
"iam:ListRoles",
"iam:ListUserPolicies",
"iam:ListUsers",
"inspector2:BatchGetAccountStatus",
"inspector2:ListCoverage",
"inspector2:ListFindings",
"lambda:GetFunctionConfiguration",
"lambda:GetPolicy",
"lambda:ListFunctionUrlConfigs",
"lambda:ListFunctions",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
"logs:ListTagsForResource",
"pi:DescribeDimensionKeys",
"pi:GetResourceMetrics",
"rds:DescribeDBClusters",
"rds:DescribeDBInstances",
"rds:DescribeDBParameters",
"rds:DescribeEvents",
"rds:DescribePendingMaintenanceActions",
"s3:GetAccountPublicAccessBlock",
"s3:GetBucketAcl",
"s3:GetBucketLocation",
"s3:GetBucketLogging",
"s3:GetBucketPolicyStatus",
"s3:GetBucketPublicAccessBlock",
"s3:GetBucketVersioning",
"s3:GetEncryptionConfiguration",
"s3:ListAllMyBuckets",
"securityhub:DescribeHub",
"securityhub:GetEnabledStandards",
"securityhub:GetFindings",
"sts:GetCallerIdentity",
"support:DescribeTrustedAdvisorCheckResult",
"support:DescribeTrustedAdvisorCheckSummaries",
"support:DescribeTrustedAdvisorChecks"
],
"Resource": "*"
}
]
}You do not need all of these. Every check degrades independently: if a permission is missing, that check reports “Unable to evaluate — insufficient permissions”, and where AWS tells us which action was missing, the dashboard names it. A missing permission is never reported as “secure” or “no findings”.
The AWS managed policies ReadOnlyAccess or SecurityAudit (plus ce:* read
actions for billing, and a Business/Enterprise support plan for Trusted Advisor)
also work.
Notable requirements:
- Cost Explorer must be enabled in the payer account, and
ce:GetCostAndUsagecharges per request — see AWS API usage accounting. - Trusted Advisor via the Support API requires a Business, Enterprise On-Ramp or Enterprise support plan.
- Compute Optimizer, Security Hub, GuardDuty, Inspector, Access Analyzer and AWS Config must be enabled per account/region to return data. When they are not, the dashboard says so rather than implying a clean result.
- Performance Insights (
pi:*) is only useful where it is enabled on the database; where it is not, the drill-down says so. Container Insights and the CloudWatch agent are the same story for ECS per-task metrics and EC2 memory. cloudwatch:GetMetricDatais billed per metric requested. Infrastructure monitoring batches every resource in a region into one call and asks for a handful of metrics per resource; the full set is only fetched for a resource you open.
The read-only guarantee
This is the central design property of the package.
Every AWS call is made through AwsAccessLayer (src/aws/access-layer.ts). No
service module holds an SDK client directly. Before a request is dispatched, three
independent barriers are applied in src/aws/readonly-guard.ts:
- Mutating-verb denylist — an operation beginning with
Create,Delete,Update,Put,Modify,Attach,Invoke,Start,Stop,Terminate,Authorize,Revoke, … is refused outright. - Read-verb allowlist — the operation must begin with a recognised read verb
(
Get,List,Describe,Lookup,Search,Filter,BatchGet, …). - Operation allowlist — the operation must appear in the compile-time allowlist
for that specific AWS service (
src/aws/allowlist.ts).
The allowlist is a constant that is deep-frozen at module load. It is never read from disk, never merged with user configuration, and never mutated at runtime. You may narrow what the dashboard calls by disabling API categories in Settings; there is no code path by which any configuration file, environment variable or UI action can add an operation.
If enforcement refuses an operation, the request is rejected before an SDK call is constructed — so a refused mutation is not even counted as an AWS call.
You can see the full allowlist in the running dashboard under Settings → AWS
read-only guarantee → Show the allowlist, or via GET /api/permissions.
The test suite asserts this directly: write operations across every service are rejected, the allowlist cannot be widened at runtime, and a hand-edited configuration cannot bypass enforcement.
AWS API usage accounting
The header shows a live count:
AWS API Calls: 1,248Click it to open AWS API Usage, which breaks the total down by category — Billing / Cost Explorer, Security Hub, GuardDuty, Inspector, IAM, S3, Lambda, Security Groups, CloudWatch, CloudTrail, Compute Optimizer, AWS Config, Trusted Advisor, IAM Access Analyzer — and lists recent calls with their AWS service and operation, profile, account, region, duration, outcome, and the dashboard action that caused them.
Why it exists: some AWS read APIs are billed (Cost Explorer requests, for example) and all of them consume request quota. This counter lets you see exactly how much AWS activity the dashboard itself generates, rather than guessing.
The tracker sits inside the same access layer as read-only enforcement, so every AWS request is both enforced and counted. Statistics are session based and reset when the server restarts, because raw AWS data is never persisted.
AI provider calls are not AWS API calls and are deliberately excluded from this counter.
What costs money
Almost everything this dashboard calls is free: describing an instance costs nothing, whatever the instance costs. Three things are not, and the UI says so before you click rather than after the invoice arrives.
| API | What AWS charges | Where it is used |
| --- | --- | --- |
| Cost Explorer — GetCostAndUsage, GetDimensionValues, GetCostCategories, GetTags | $0.01 per request, with no free tier | Billing / Cost |
| CloudWatch — GetMetricData | $0.01 per 1,000 metrics requested — not per call — and excluded from the CloudWatch free tier | Infrastructure, CloudWatch |
| CloudWatch — GetMetricStatistics, ListMetrics | $0.01 per 1,000 requests beyond the free tier | CloudWatch |
| S3 — ListBuckets, GetBucket* | fractions of a cent per request; the rate depends on the request and storage class | Security |
Everything else — ECS, EC2, RDS, ElastiCache, ELB, CloudTrail LookupEvents,
GuardDuty, Inspector, Config, Compute Optimizer, IAM, STS, and the Performance
Insights API at this volume — is free to call.
How it appears:
- a
$beside Billing / Cost, Security, Infrastructure, CloudWatch and AI Insights in the sidebar, before any of them is opened; - a tag next to the section title and on its card in the Overview, whose tooltip gives the price, its basis and the date it was checked;
- a line under the section header naming the charge, linking to AWS's own pricing page, and — once a scan has run — what that section has actually spent this session;
- AWS API Usage counts billed calls and billed units separately (one
GetMetricDatacall for 400 metrics is 400 units), estimates the session total, and lists the full price table with its sources.
Every figure is an estimate at US East (N. Virginia) list price, with the date
it was checked shown beside it. Your region, the free tier, credits and any private
pricing all change the real number. Where AWS's price depends on something the
dashboard cannot see, it says the call is billed and gives no figure rather than
inventing one — and a charge too small to display is shown as < $0.01, never as
$0.00.
The Analyze button in AI Insights carries the same tag for a different reason: the request goes to your AI provider, which may bill for it, and a selected section with no data in memory is fetched first.
Configuration
Configuration lives in a small set of JSON files (no SQLite, nothing hidden) under
~/.aws-readonly-dashboard, or wherever --config-dir / AWS_READONLY_DASHBOARD_HOME
points:
| File | Contents |
| --- | --- |
| config.json | Selected profiles and regions, thresholds, filters, UI and CloudWatch preferences, AI provider configuration, sanitization rules, AI history settings |
| security-findings.json | Your decisions about findings (acknowledged / ignored / resolved) plus the minimum finding identity needed to keep them filterable |
| ai-history.json | Sanitized AI history — only when you enable it |
Files are written atomically (temp file + rename) with owner-only permissions
(0600, directory 0700), and carry a schema version so future releases can migrate
them safely. A configuration file that is corrupt, hand-edited into an invalid shape,
or contains out-of-range values is normalised rather than trusted: unknown keys are
dropped, numbers are clamped, and sanitization rules that protect credentials are
restored.
AWS data is never written to disk. It lives in memory for the lifetime of the server process.
Settings → Local data → Delete All Local Configuration removes every file above and resets the application to defaults.
AI insights
Gemini CLI (default)
At startup the dashboard checks whether the Gemini CLI
is available by running gemini --version. That is a capability probe, not an AI
call — no prompt and no data leave your machine.
The header reports one of three states, and they mean different things:
| State | Meaning | What to do |
| --- | --- | --- |
| Gemini CLI: Available | The CLI was found and ran. | Nothing. |
| Gemini CLI: Not authenticated | The CLI is installed and runs, but has no usable credentials. | Run gemini once and sign in, or set GEMINI_API_KEY. |
| Non-LLM mode | No CLI was found on PATH (or the configured command could not be executed). | Install it, point Settings at the right command, or use a custom endpoint. |
"Installed but not signed in" is deliberately not collapsed into "not installed" —
the fix is different, so the message is different. The probe reports why it failed,
including the specific ENOENT / EACCES cases, rather than a generic failure.
The dashboard does not install Gemini, does not call it on your behalf, and does not silently switch to another provider. In Non-LLM mode every AWS feature still works, and you can still preview exactly what would be sent.
Windows. The CLI installs as gemini.cmd, and Node cannot execute a .cmd or
.bat file directly. The dashboard resolves the command against PATH and
PATHEXT and only then decides whether a shell is required, so gemini works on
Windows exactly as it does on macOS and Linux. Because a shell is involved in that
case, a configured command containing shell metacharacters is refused rather than
executed — configure the plain path to the executable and use Settings for
arguments.
Authentication is entirely the CLI's: the dashboard invokes it as a child process and inherits your existing session. No Gemini credentials or API keys are stored by this application. The prompt is written to the CLI's standard input, never onto a command line — that keeps it out of your shell history and process list, and it also means a large payload cannot hit the Windows 8191-character command-line limit.
Custom LLM REST endpoint
For teams with an internal LLM gateway, Settings → Custom LLM REST endpoint lets you configure:
- endpoint URL and HTTP method
- arbitrary headers (including authentication headers)
- a request body template, using
{{prompt}}(the sanitized prompt as a JSON string) and{{payload}}(the sanitized payload as raw JSON) - a response path such as
choices.0.message.content, for APIs that wrap the text - enable / disable, independent of Gemini
Because the template is yours, the provider works with APIs that are not OpenAI- or Gemini-compatible.
The endpoint you configure receives the sanitized, pseudonymised payload. You are responsible for trusting it. The dashboard will not send anything until you configure the endpoint, enable it, tick the acknowledgement, and press an Analyze button.
AI calls are never automatic
AI is never invoked during startup, data loading, section loading, refresh, Refresh All, CloudTrail search, security scanning, billing retrieval, background polling or page navigation. It runs only on an explicit action:
- Analyze Billing, Analyze Security, Analyze CloudWatch, Analyze CloudTrail, Analyze Compute Optimizer
- Analyze Selected Data — pick several sections and let the model look for relationships between them (for example, a cost increase that lines up with a change visible in CloudTrail). The model is instructed not to assert causation that the evidence does not support.
In CloudTrail you can tick individual events and send only those.
AI request batching
A whole-account payload is tens of thousands of characters, and a provider handed all of it at once routinely takes longer than the 120-second default timeout — which is how "the AI never works" happens.
So an analysis is split. The payload is sanitized once and then divided into
requests of at most ai.batching.maxPayloadChars characters (40 000 by default),
ordered worst-first so the critical findings are in the first request. Sanitizing each
request separately would let <WORKLOAD_1> mean one resource in one request and a
different one in the next, so it is deliberately done before the split.
While it runs you get a progress bar ("batch 2 of 5"), the findings from the batches that have already come back, and a Stop button. A batch that fails costs its own evidence and nothing else: the result says "4 of 5 batches analysed" and names what was missed, rather than quietly returning less. The one exception is the first batch — if that fails, the provider is unusable and the analysis fails outright rather than pretending to be partial.
After the batches, one small extra request over just the batch summaries produces the overall summary and the cross-section correlations, which cannot be seen from inside a single batch. If that pass fails, the batch findings stand and the summary says it could not be produced.
All of it is configurable in Settings → AI request batching: characters per request, maximum requests, how many run at once (one by default — a CLI provider starts a process per call), and whether the final pass runs at all.
Grounding
Prompts are versioned (src/ai/prompts.ts) and instruct the model to use only the
supplied evidence, to quote the evidence behind every finding, to treat
“not evaluated” entries as unknown rather than healthy, and to state its limitations.
Responses are requested as structured JSON and validated before anything is
rendered; severities are constrained to critical | high | medium | low. When AI
analysis is requested, the model's severity replaces the dashboard's own.
If a response is malformed, the dashboard shows a clear error and keeps your AWS data and dashboard state intact. It never fabricates a response and never silently retries against a different provider.
AI history
Off by default. When enabled:
- default retention 7 days, maximum 30, configurable
- stored as JSON, and expired entries are removed automatically
- only the sanitized prompt, the sanitized payload, the provider response, timestamp, profiles, regions and selected sections are stored — never raw AWS data, and never the placeholder→value mapping
- the UI states clearly that history is enabled and may be used for future analyses
- relevant history is scoped by profile: analysing
devnever pulls inprdhistory - Delete AI history is available in Settings and in AI Insights
Privacy
The AI request pipeline is fixed:
AWS data → relevant-section selection → aggregation/compaction →
PII sanitization → pseudonymization → AI providerThere is no path to a provider that skips sanitization.
By default the following never leave your machine:
| Category | Treatment |
| --- | --- |
| AWS access keys, secrets, tokens | Redacted — always on, cannot be disabled |
| Email addresses | Redacted → <EMAIL> |
| IP addresses | Redacted → <IP> (but 0.0.0.0/0 and ::/0 are kept: they are policy values, not identifiers) |
| AWS account IDs | Pseudonymised → <ACCOUNT_1> |
| IAM user names | Pseudonymised → <USER_1> |
| IAM role names | Pseudonymised → <ROLE_1> |
| ARNs | Partially anonymised — service, region and resource type kept; account and resource name replaced |
| Hostnames | Redacted → <HOST> (AWS service endpoints such as ec2.amazonaws.com are kept) |
| Instance / volume / ENI identifiers | Pseudonymised → <RESOURCE_1> |
| Workload names — ECS services and clusters, databases, caches, load balancers | Pseudonymised → <WORKLOAD_1>, including inside identifiers AWS derives from them, such as an ALB's DNS name or a cache node id |
Deliberately preserved, because they carry infrastructure meaning without identifying a person: S3 bucket names, Lambda function names and security group IDs.
Pseudonymisation is consistent within a request — the same entity always gets the same placeholder, so the model can reason about relationships — and the placeholder→value mapping stays on your machine. It is shown in the UI so you can decode the model's output, and it is never sent to a provider or written to history.
In Settings → AI data privacy you can disable default rules and add your own regular-expression rules (redacting or pseudonymising). Disabling a rule is explicitly labelled as a choice to allow that category of data through. Payload preview shows exactly what would be sent, before you send it.
Security
- The server binds to
127.0.0.1by default; it is not reachable from your network unless you pass--host, and the CLI warns you when you do. - AWS credentials are never read into the application's own state, never logged and never persisted. The AWS SDK's own credential providers resolve them.
- The logger refuses to print values under credential-shaped keys and redacts credential-shaped values wherever they appear.
- AI payload bodies are never logged.
- Errors shown in the UI are sanitized AWS messages, not stack traces.
- Local files are written atomically with owner-only permissions.
- The dashboard page is served with a restrictive Content-Security-Policy and loads no third-party scripts, fonts or styles.
See SECURITY.md for the vulnerability reporting process and the full security model.
Uninstalling
npm uninstall -g aws-readonly-dashboardnpm removes the package but does not touch your data. To remove everything this application stored:
rm -rf ~/.aws-readonly-dashboardOr, before uninstalling, use Settings → Delete All Local Configuration in the dashboard, which removes the same directory.
That directory is the only place the application writes. It never writes to your AWS configuration, and it never writes AWS data anywhere.
Troubleshooting
No AWS profiles discovered
Run aws configure (or aws configure sso), or export AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY. The dashboard reads ~/.aws/config and ~/.aws/credentials.
“Unable to evaluate — authentication failure”
Your credentials are missing or expired. For SSO profiles, run
aws sso login --profile <name> and press Refresh.
“Unable to evaluate — insufficient permissions” The dashboard names the missing IAM action where AWS provides it. Add that action, or leave it out and accept that the check stays unevaluated — it will never be reported as a pass.
Cost Explorer returns nothing / access denied
Cost Explorer must be enabled (once, in the payer account) and can take up to 24
hours to populate. Member accounts may not see data. You need ce:GetCostAndUsage.
Trusted Advisor checks are unavailable The Support API requires a Business, Enterprise On-Ramp or Enterprise support plan. Without one, these checks are reported as unevaluated.
CloudTrail search returns fewer events than expected
LookupEvents covers the last 90 days of management events and AWS evaluates one
lookup attribute per request; the remaining filters are applied locally while
paging. Narrow the time window or add a filter if a search is truncated.
Infrastructure metrics say “not collected”
CloudWatch returned no datapoints for that metric. The dashboard states the reason
where one is known: EC2 publishes no memory metric without the CloudWatch agent, ECS
publishes per-task metrics only with Container Insights enabled, ReplicaLag exists
only on read replicas, and BurstBalance only on gp2 volumes. It reports the gap
rather than drawing a zero.
A resource shows as “Unknown”
None of its metrics came back. That is different from healthy, so it is never shown
as healthy. Check that cloudwatch:GetMetricData is permitted and that the resource
has been running long enough to publish anything.
Performance Insights is empty for a database
It is enabled per instance, keeps 7 days by default, and is addressed by the
database's DbiResourceId rather than its name. The panel states which of those
applies rather than showing an empty chart.
An event is shown in AWS's own words No translator matched it, so it is passed through verbatim and labelled as not translated rather than guessed at. The Explain events button reports how many patterns are currently translated.
Infrastructure only lists some of my resources A per-type cap (100 by default) keeps the first click cheap. When it truncates a scan, the dashboard says so and offers a one-click “inspect everything” escalation; the cap is also in Settings → Infrastructure.
Compute Optimizer shows nothing The account must be opted in, and AWS needs enough metric history (typically 14 days of CloudWatch data) before it will make a recommendation. The dashboard shows the enrollment status rather than inventing recommendations.
“Non-LLM mode”
The Gemini CLI was not found on PATH. Install it (npm install -g @google/gemini-cli),
adjust the command in Settings → AI, or configure a custom LLM endpoint.
Everything except AI analysis works normally.
On Windows the CLI is gemini.cmd; the dashboard resolves that through PATH and
PATHEXT itself, so you do not need to configure the .cmd suffix. If you point
Settings at an absolute path, point it at the real executable — a command containing
shell metacharacters is refused rather than run through a shell.
“Cannot use both a positional prompt and the --prompt (-p) flag together”
A Windows-only fault in 1.1.0, fixed in 1.2.0: the CLI was launched through a shell,
and Node does not quote arguments when it does that, so a multi-word instruction
arrived as a flag followed by several positional arguments. Upgrade with
npm install -g aws-readonly-dashboard@latest.
“Gemini CLI: Not authenticated”
The CLI is installed and runs, but has no credentials. Run gemini once in a
terminal and complete the sign-in, or set GEMINI_API_KEY in the environment the
dashboard is started from. This is reported separately from Non-LLM mode on purpose:
installing the CLI again will not fix it.
Custom LLM requests fail Check the endpoint URL, headers and response path in Settings. A configured response path that does not exist in the response is reported as an error rather than being guessed around. Use Preview payload to inspect the exact request first.
“Unable to evaluate — network or TLS failure”
The request never reached AWS. The usual cause is a corporate proxy that re-signs
TLS traffic: the AWS CLI reads HTTPS_PROXY automatically, the AWS SDK for
JavaScript does not, so the CLI can work where the dashboard does not. The exact
cause — certificate, connection or DNS — is printed under the label.
An “unexpected error” with no explanation Every "unable to evaluate" row now prints the underlying message underneath it. If a row still says only unexpected error, the message beneath it is what AWS or the dashboard actually reported; include that line in a bug report.
“ListBuckets on s3 in global” times out
Fixed in 1.3.0. Without a page size, S3 assembles the account's entire bucket
inventory into one response, which on a large account takes longer than the request
deadline — so the check failed before examining a single bucket. The listing is now
paged (1,000 buckets per request, stopping as soon as the scan limit is reached), and
each bucket's region comes from the listing itself, which also removes one
GetBucketLocation call per bucket.
If a listing still times out, the message says how far it got — page 2, 1000 buckets
so far. Page 1, 0 buckets means the request never got an answer at all, which is a
network or proxy problem rather than a size one. Raise Settings → Scan limits and
AWS requests → AWS request timeout for a slow network, and see AWS API Usage →
Slowest calls for which requests are near the limit.
S3 checks report timeouts, or say a permission is missing when it is not
A failed AWS call is reported by what actually failed. "Request timed out" means
the request did not come back in time — it is not a statement about your IAM policy,
and the dashboard no longer adds a "needs s3:Get…" note to one. If you see
timeouts here:
- Buckets are read one region at a time. The dashboard resolves each bucket's real
region first (including S3's legacy
EUandUSlocation values) and asks only that region; a bucket whose region cannot be resolved is reported as partially evaluated rather than queried in a guessed region. - Check AWS API Usage to confirm the calls are being made with the profile you selected — every call is listed with its profile, account and region.
- A corporate proxy is the usual cause of timeouts that the AWS CLI does not show:
the CLI honours
HTTPS_PROXYautomatically, the AWS SDK for JavaScript does not. - Raise the per-request deadline only if your network is genuinely slow; a timed-out request is now cancelled rather than left running, so a slow endpoint no longer starves later calls of connections.
Only some Lambda functions or S3 buckets were inspected
Scans are bounded so they cannot silently cost thousands of AWS calls. The notice
that reports partial coverage carries a button — Inspect every function, Inspect
all N buckets — that raises the limit and rescans; results stream in as the scan
runs. The same limits live in Settings → Security scan limits, where 0 for the
Lambda lookups means "no limit". To switch a check off entirely, disable it under
Security checks instead.
A bucket is listed as "partially evaluated" One of the five reads for that bucket did not return a definitive answer, so the dashboard will not state whether it is protected. It is never counted as secure, and no finding is raised from a read that failed.
Port conflicts
The dashboard finds the next free port automatically and prints it. Use --port to
choose a different starting point.
Diagnosing anything else
Start the dashboard with --log-level debug and every AWS call is logged with its
service, operation, region, profile and duration; listings also log the page they
reached. Any call slower than 10 seconds is logged as a warning at the default log
level, before it becomes a timeout.
High AWS API call counts Open AWS API Usage to see exactly which category is responsible. The usual culprits are per-resource checks: Lambda resource policies (one call per function) and S3 bucket settings (up to five calls per bucket). Tune them under Settings → Security scan limits, reduce the number of selected regions, or disable API categories you do not need.
"Partially evaluated — N of M inspected" A scan limit stopped the check before it covered the whole inventory. The resources that were not inspected have an unknown status, not a clean one. Raise the limit in Settings → Security scan limits to cover them; the cost is more AWS API calls. The "Lambda functions outside a VPC" check is never limited — it always covers every function and reports them as a single aggregated finding per region.
Lots of "expected not found" calls in AWS API Usage That is normal. Several AWS APIs answer "not found" for an ordinary state — a bucket with no policy, a Lambda function with no resource policy — and the counter shows these separately from failures that need attention.
Development
git clone https://github.com/Ashutosh-kumar07/aws-readonly-dashboard.git
cd aws-readonly-dashboard
npm install
npm run dev # start the dashboard from TypeScript sources
npm test # run the test suite
npm run test:watch # watch mode
npm run lint # ESLint
npm run typecheck # tsc --noEmit
npm run format # Prettier
npm run build # compile to dist/ and verify the entry points
npm run verify # lint + typecheck + test + buildThe architecture is deliberately layered:
CLI (src/cli.ts)
└─ Local server (src/server/)
└─ Dashboard API (src/server/routes.ts)
└─ Partial-result job runner (src/server/job-runner.ts)
└─ Application services (src/server/dashboard-service.ts, src/services/)
└─ AWS read-only access layer (src/aws/)
├─ read-only enforcement (readonly-guard.ts)
├─ operation allowlist (allowlist.ts)
├─ API call tracker (tracker.ts)
├─ credential/profile context (profiles.ts)
└─ AWS SDK
Application services
└─ AI orchestrator (src/ai/orchestrator.ts)
├─ Gemini CLI provider (src/ai/providers/gemini-cli.ts)
│ └─ cross-platform command resolver (providers/command-resolver.ts)
└─ Custom REST provider (src/ai/providers/custom-rest.ts)
Security analyzer (src/services/security/)
└─ one self-contained module per check in checks/, registered in registry.tsAdding a security check is a new file in src/services/security/checks/ plus one
line in registry.ts; adding an AWS operation requires adding it to the allowlist,
which is the only place that can grant access.
The frontend in public/ is dependency-free ES modules — no build step, no
framework, no third-party runtime code.
Contributing
Contributions are welcome. Please read CONTRIBUTING.md and the Code of Conduct. Changes that would weaken the read-only guarantee, the sanitization pipeline or the "AI is never automatic" rule will not be accepted.
Publishing is documented in PUBLISHING.md.
