retirement-radar 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/package.json +36 -0
- package/retirements.yaml +68 -0
- package/src/cli.js +134 -0
- package/src/radar.js +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Het Patel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<p align="center"><img src="https://raw.githubusercontent.com/Het101/retirement-radar/main/docs/banner.png" alt="Retirement Radar: find AWS resources before end of support costs you money" width="100%"></p>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/retirement-radar"><img src="https://img.shields.io/npm/v/retirement-radar?color=E5484D&label=npm" alt="npm version"></a>
|
|
5
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3f4756" alt="MIT license"></a>
|
|
6
|
+
<img src="https://img.shields.io/badge/AWS-read--only-3f4756" alt="Read-only">
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
# Retirement Radar
|
|
10
|
+
|
|
11
|
+
Find what in your AWS account is about to lose support before it starts costing you money or gets force-upgraded.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx retirement-radar scan
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
<img src="https://raw.githubusercontent.com/Het101/retirement-radar/main/docs/scan-output.png" alt="Example output: a table of resources with status RETIRED, EXTENDED, SOON or UPCOMING, the end-of-support date and what it means, plus the monthly EKS extended support cost" width="100%">
|
|
18
|
+
|
|
19
|
+
<sub>Example output with sample data.</sub>
|
|
20
|
+
|
|
21
|
+
## What it checks
|
|
22
|
+
|
|
23
|
+
| Service | What | Why it matters |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| EKS | Kubernetes version per cluster | After standard support, clusters move to extended support at 6x the control-plane price, then get force-upgraded |
|
|
26
|
+
| RDS / Aurora | Engine major version (PostgreSQL, MySQL, Aurora) | After standard support, RDS bills Extended Support per vCPU-hour automatically |
|
|
27
|
+
| Lambda | Function runtime | Deprecated runtimes get no security patches, then can't be created or updated |
|
|
28
|
+
|
|
29
|
+
Dates live in [`retirements.yaml`](retirements.yaml), each section with its AWS source link. A version that isn't listed shows as `unknown`, never as fine.
|
|
30
|
+
|
|
31
|
+
## Run it
|
|
32
|
+
|
|
33
|
+
You need Node 18+ and the [AWS CLI v2](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html), signed in (`aws configure`, `aws sso login`, or an assumed role). Retirement Radar uses your AWS CLI, so profiles, SSO and roles just work.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx retirement-radar scan
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Option | |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `--region eu-west-1,us-east-1` | Only these regions (default: every enabled region) |
|
|
42
|
+
| `--profile prod` | AWS CLI profile |
|
|
43
|
+
| `--json` | Machine-readable output |
|
|
44
|
+
| `--all` | Also list resources with more than a year left |
|
|
45
|
+
| `--fail-on soon` | Exit 1 if anything is `retired`, `extended` or `soon` (levels: `retired`, `extended`, `soon`, `upcoming`) |
|
|
46
|
+
| `--data my.yaml` | Use your own dates file |
|
|
47
|
+
|
|
48
|
+
## Safe by design
|
|
49
|
+
|
|
50
|
+
- **Read-only.** It only calls `sts get-caller-identity`, `ec2 describe-regions`, `eks list-clusters` / `describe-cluster`, `rds describe-db-instances` / `describe-db-clusters` and `lambda list-functions`.
|
|
51
|
+
- **Local.** Nothing is sent anywhere; no telemetry.
|
|
52
|
+
- A region or service you can't read becomes a warning, not a failed scan.
|
|
53
|
+
|
|
54
|
+
Minimal IAM policy:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"Version": "2012-10-17",
|
|
59
|
+
"Statement": [{
|
|
60
|
+
"Effect": "Allow",
|
|
61
|
+
"Action": ["ec2:DescribeRegions", "eks:ListClusters", "eks:DescribeCluster",
|
|
62
|
+
"rds:DescribeDBInstances", "rds:DescribeDBClusters", "lambda:ListFunctions"],
|
|
63
|
+
"Resource": "*"
|
|
64
|
+
}]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## In CI
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
# .github/workflows/retirement-radar.yml - weekly, fails when something needs action within 90 days
|
|
72
|
+
on: { schedule: [{ cron: "0 8 * * 1" }], workflow_dispatch: {} }
|
|
73
|
+
permissions: { id-token: write, contents: read }
|
|
74
|
+
jobs:
|
|
75
|
+
scan:
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
79
|
+
with: { role-to-assume: arn:aws:iam::123456789012:role/retirement-radar, aws-region: us-east-1 }
|
|
80
|
+
- run: npx retirement-radar scan --fail-on soon
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Contributing dates
|
|
84
|
+
|
|
85
|
+
Found a wrong or missing date? Edit `retirements.yaml`, include the AWS source link in the PR, and run `npm test`.
|
|
86
|
+
|
|
87
|
+
## Roadmap
|
|
88
|
+
|
|
89
|
+
Azure and GCP retirements, ElastiCache / OpenSearch / MSK versions, hosted weekly scans with alerts, cost per finding. Part of [HetOps](https://hetops.dev).
|
|
90
|
+
|
|
91
|
+
MIT licensed.
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "retirement-radar",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Read-only scan of your AWS account for EKS, RDS and Lambda versions near end of support.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"retirement-radar": "src/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"retirements.yaml"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node --test",
|
|
14
|
+
"prepublishOnly": "node --test"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"aws",
|
|
21
|
+
"eks",
|
|
22
|
+
"rds",
|
|
23
|
+
"lambda",
|
|
24
|
+
"end-of-life",
|
|
25
|
+
"deprecation",
|
|
26
|
+
"devops"
|
|
27
|
+
],
|
|
28
|
+
"repository": "github:Het101/retirement-radar",
|
|
29
|
+
"homepage": "https://hetops.dev",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"yaml": "^2.5.0"
|
|
33
|
+
},
|
|
34
|
+
"author": "Het Patel (https://hetops.dev)",
|
|
35
|
+
"bugs": "https://github.com/Het101/retirement-radar/issues"
|
|
36
|
+
}
|
package/retirements.yaml
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# End-of-support dates Retirement Radar checks against.
|
|
2
|
+
# Every section links its source. Found a wrong or missing date? Open a PR with the source link.
|
|
3
|
+
# Versions not listed here show up as "unknown", never as safe.
|
|
4
|
+
updated: "2026-10-04"
|
|
5
|
+
|
|
6
|
+
eks:
|
|
7
|
+
source: https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html
|
|
8
|
+
# After standard support ends, clusters move to extended support automatically and cost more.
|
|
9
|
+
# After extended support ends, AWS upgrades the control plane for you.
|
|
10
|
+
cost: Extended support adds $0.50 per cluster-hour, about $365 a month per cluster.
|
|
11
|
+
monthlyCost: 365
|
|
12
|
+
versions:
|
|
13
|
+
"1.23": { standard: "2023-10-11", extended: "2024-10-11" }
|
|
14
|
+
"1.24": { standard: "2024-01-31", extended: "2025-01-31" }
|
|
15
|
+
"1.25": { standard: "2024-05-01", extended: "2025-05-01" }
|
|
16
|
+
"1.26": { standard: "2024-06-11", extended: "2025-06-11" }
|
|
17
|
+
"1.27": { standard: "2024-07-24", extended: "2025-07-24" }
|
|
18
|
+
"1.28": { standard: "2024-11-26", extended: "2025-11-26" }
|
|
19
|
+
"1.29": { standard: "2025-03-23", extended: "2026-03-23" }
|
|
20
|
+
"1.30": { standard: "2025-07-23", extended: "2026-07-23" }
|
|
21
|
+
"1.31": { standard: "2025-11-26", extended: "2026-11-26" }
|
|
22
|
+
"1.32": { standard: "2026-03-23", extended: "2027-03-23" }
|
|
23
|
+
"1.33": { standard: "2026-07-29", extended: "2027-07-29" }
|
|
24
|
+
|
|
25
|
+
rds:
|
|
26
|
+
source: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/extended-support.html
|
|
27
|
+
# After standard support ends, RDS enrols the database in Extended Support and bills it per vCPU-hour.
|
|
28
|
+
cost: RDS Extended Support is billed per vCPU-hour on top of the instance price.
|
|
29
|
+
engines:
|
|
30
|
+
postgres:
|
|
31
|
+
"11": { standard: "2024-02-29" }
|
|
32
|
+
"12": { standard: "2025-02-28" }
|
|
33
|
+
"13": { standard: "2026-02-28" }
|
|
34
|
+
"14": { standard: "2027-02-28" }
|
|
35
|
+
"15": { standard: "2028-02-29" }
|
|
36
|
+
"16": { standard: "2029-02-28" }
|
|
37
|
+
mysql:
|
|
38
|
+
"5.7": { standard: "2024-02-29" }
|
|
39
|
+
"8.0": { standard: "2026-07-31" }
|
|
40
|
+
aurora-postgresql:
|
|
41
|
+
"11": { standard: "2024-02-29" }
|
|
42
|
+
"12": { standard: "2025-02-28" }
|
|
43
|
+
"13": { standard: "2026-02-28" }
|
|
44
|
+
"14": { standard: "2027-02-28" }
|
|
45
|
+
"15": { standard: "2028-02-29" }
|
|
46
|
+
"16": { standard: "2029-02-28" }
|
|
47
|
+
aurora-mysql:
|
|
48
|
+
"5.7": { standard: "2024-10-31" }
|
|
49
|
+
|
|
50
|
+
lambda:
|
|
51
|
+
source: https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html
|
|
52
|
+
# Deprecated runtimes stop getting security patches; AWS then blocks creating and later updating functions.
|
|
53
|
+
runtimes:
|
|
54
|
+
nodejs14.x: "2023-12-04"
|
|
55
|
+
nodejs16.x: "2024-06-12"
|
|
56
|
+
nodejs18.x: "2025-09-01"
|
|
57
|
+
nodejs20.x: "2026-04-30"
|
|
58
|
+
python3.7: "2023-12-04"
|
|
59
|
+
python3.8: "2024-10-14"
|
|
60
|
+
python3.9: "2025-12-15"
|
|
61
|
+
ruby2.7: "2023-12-07"
|
|
62
|
+
ruby3.2: "2026-03-31"
|
|
63
|
+
java8: "2024-01-08"
|
|
64
|
+
go1.x: "2024-01-08"
|
|
65
|
+
provided: "2024-01-08"
|
|
66
|
+
dotnet6: "2024-12-20"
|
|
67
|
+
dotnet7: "2024-05-14"
|
|
68
|
+
dotnet8: "2026-11-10"
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// retirement-radar scan: read-only. Runs the AWS CLI you already have (your profiles, SSO, roles),
|
|
3
|
+
// calls only list/describe APIs, and prints what is near end of support. Nothing is sent anywhere.
|
|
4
|
+
const { execFile } = require('node:child_process');
|
|
5
|
+
const { SEVERITY, loadRetirements, evaluate, sortFindings, failsAt } = require('./radar');
|
|
6
|
+
|
|
7
|
+
const HELP = `Usage: retirement-radar scan [options]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--region a,b Regions to scan (default: every enabled region)
|
|
11
|
+
--profile name AWS CLI profile
|
|
12
|
+
--json Print findings as JSON
|
|
13
|
+
--all Also list resources that are fine
|
|
14
|
+
--fail-on level Exit 1 if anything is at this level or worse: ${SEVERITY.slice(0, 4).join(', ')}
|
|
15
|
+
--data file Use your own retirements.yaml
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const o = { cmd: argv[0] };
|
|
20
|
+
for (let i = 1; i < argv.length; i++) {
|
|
21
|
+
const a = argv[i];
|
|
22
|
+
if (a === '--json' || a === '--all') o[a.slice(2)] = true;
|
|
23
|
+
else if (['--region', '--profile', '--fail-on', '--data'].includes(a)) o[a.slice(2)] = argv[++i];
|
|
24
|
+
else throw new Error(`Unknown option ${a}`);
|
|
25
|
+
}
|
|
26
|
+
return o;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function awsRunner(profile) {
|
|
30
|
+
return (args, region) => new Promise((resolve, reject) => {
|
|
31
|
+
const full = [...args, '--output', 'json', ...(region ? ['--region', region] : []), ...(profile ? ['--profile', profile] : [])];
|
|
32
|
+
execFile('aws', full, { maxBuffer: 64 * 1024 * 1024, windowsHide: true }, (err, stdout, stderr) => {
|
|
33
|
+
if (err && err.code === 'ENOENT') return reject(new Error('AWS CLI not found. Install AWS CLI v2 and sign in (aws configure / aws sso login).'));
|
|
34
|
+
if (err) return reject(new Error((stderr || err.message).trim().split('\n').pop()));
|
|
35
|
+
try { resolve(JSON.parse(stdout || '{}')); } catch { reject(new Error('Unexpected AWS CLI output')); }
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DB_ENGINE = /^(aurora|mysql|postgres|mariadb|oracle|sqlserver)/;
|
|
41
|
+
|
|
42
|
+
async function collectRegion(region, aws, warn) {
|
|
43
|
+
const items = [];
|
|
44
|
+
const step = async (label, fn) => { try { await fn(); } catch (e) { warn(`${region} ${label}: ${e.message}`); } };
|
|
45
|
+
await Promise.all([
|
|
46
|
+
step('EKS', async () => {
|
|
47
|
+
const { clusters = [] } = await aws(['eks', 'list-clusters'], region);
|
|
48
|
+
for (const name of clusters) {
|
|
49
|
+
const { cluster } = await aws(['eks', 'describe-cluster', '--name', name], region);
|
|
50
|
+
items.push({ service: 'EKS', region, resource: name, version: cluster.version });
|
|
51
|
+
}
|
|
52
|
+
}),
|
|
53
|
+
step('RDS', async () => {
|
|
54
|
+
const [{ DBInstances = [] }, { DBClusters = [] }] = await Promise.all([
|
|
55
|
+
aws(['rds', 'describe-db-instances'], region), aws(['rds', 'describe-db-clusters'], region)]);
|
|
56
|
+
// Cluster members share the cluster's engine version: report the cluster once.
|
|
57
|
+
for (const i of DBInstances) if (!i.DBClusterIdentifier && DB_ENGINE.test(i.Engine)) {
|
|
58
|
+
items.push({ service: 'RDS', region, resource: i.DBInstanceIdentifier, engine: i.Engine, version: i.EngineVersion });
|
|
59
|
+
}
|
|
60
|
+
for (const c of DBClusters) if (DB_ENGINE.test(c.Engine)) {
|
|
61
|
+
items.push({ service: 'RDS', region, resource: c.DBClusterIdentifier, engine: c.Engine, version: c.EngineVersion });
|
|
62
|
+
}
|
|
63
|
+
}),
|
|
64
|
+
step('Lambda', async () => {
|
|
65
|
+
const { Functions = [] } = await aws(['lambda', 'list-functions'], region);
|
|
66
|
+
for (const f of Functions) if (f.Runtime) items.push({ service: 'Lambda', region, resource: f.FunctionName, version: f.Runtime });
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
return items;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function scan({ regions, aws, db, now = Date.now(), warn = () => {} }) {
|
|
73
|
+
if (!regions || !regions.length) {
|
|
74
|
+
const { Regions = [] } = await aws(['ec2', 'describe-regions'], 'us-east-1');
|
|
75
|
+
regions = Regions.map((r) => r.RegionName);
|
|
76
|
+
}
|
|
77
|
+
const items = (await Promise.all(regions.map((r) => collectRegion(r, aws, warn)))).flat();
|
|
78
|
+
return sortFindings(items.map((i) => evaluate(i, db, now)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Output ─────────────────────────────────────────────────────
|
|
82
|
+
const tty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
83
|
+
const paint = (code, s) => (tty ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
84
|
+
const COLOR = { retired: 31, extended: 31, soon: 33, upcoming: 36, ok: 32, unknown: 90 };
|
|
85
|
+
|
|
86
|
+
function table(findings) {
|
|
87
|
+
const rows = findings.map((f) => [f.status.toUpperCase(), f.service, f.region, f.resource,
|
|
88
|
+
f.engine ? `${f.engine} ${f.version}` : f.version, f.date ? `${f.date} (${f.days < 0 ? `${-f.days}d ago` : `in ${f.days}d`})` : '-', f.note]);
|
|
89
|
+
const head = ['STATUS', 'SERVICE', 'REGION', 'RESOURCE', 'VERSION', 'DATE', 'WHAT IT MEANS'];
|
|
90
|
+
const w = head.map((h, c) => Math.max(h.length, ...rows.map((r) => String(r[c]).length)));
|
|
91
|
+
const line = (r, status) => r.map((v, c) => {
|
|
92
|
+
if (c === r.length - 1) return v;
|
|
93
|
+
const cell = String(v).padEnd(w[c]);
|
|
94
|
+
return c === 0 && status ? paint(COLOR[status], cell) : cell;
|
|
95
|
+
}).join(' ');
|
|
96
|
+
return [paint(1, line(head)), ...rows.map((r, i) => line(r, findings[i].status))].join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function main(argv = process.argv.slice(2)) {
|
|
100
|
+
let o;
|
|
101
|
+
try { o = parseArgs(argv); } catch (e) { console.error(e.message + '\n\n' + HELP); return 2; }
|
|
102
|
+
if (o.cmd !== 'scan') { console.log(HELP); return o.cmd ? 2 : 0; }
|
|
103
|
+
const db = loadRetirements(o.data);
|
|
104
|
+
const warnings = [];
|
|
105
|
+
let findings;
|
|
106
|
+
const aws = awsRunner(o.profile);
|
|
107
|
+
try {
|
|
108
|
+
// Fail fast with one clear message instead of a warning per region.
|
|
109
|
+
const id = await aws(['sts', 'get-caller-identity']);
|
|
110
|
+
if (!o.json) console.log(paint(90, `Scanning AWS account ${id.Account} (read-only)...
|
|
111
|
+
`));
|
|
112
|
+
findings = await scan({ regions: o.region && o.region.split(','), aws, db, warn: (w) => warnings.push(w) });
|
|
113
|
+
} catch (e) { console.error(`retirement-radar: ${e.message}`); return 2; }
|
|
114
|
+
|
|
115
|
+
if (o.json) {
|
|
116
|
+
console.log(JSON.stringify({ dataUpdated: db.updated, findings, warnings }, null, 2));
|
|
117
|
+
} else {
|
|
118
|
+
const shown = o.all ? findings : findings.filter((f) => f.status !== 'ok');
|
|
119
|
+
const count = (s) => findings.filter((f) => f.status === s).length;
|
|
120
|
+
if (shown.length) console.log(table(shown) + '\n');
|
|
121
|
+
const eksExtra = findings.reduce((n, f) => n + (f.monthlyCost || 0), 0);
|
|
122
|
+
const tally = SEVERITY.map((s) => count(s) && `${count(s)} ${s}`).filter(Boolean).join(', ');
|
|
123
|
+
console.log(`${findings.length} resources scanned${tally ? `: ${tally}` : ''}.`);
|
|
124
|
+
if (eksExtra) console.log(paint(31, `EKS extended support is costing about $${eksExtra.toLocaleString()} a month.`));
|
|
125
|
+
if (count('unknown')) console.log(paint(90, 'Unknown = version not in retirements.yaml yet. PRs with a source link welcome.'));
|
|
126
|
+
if (!o.all && count('ok')) console.log(paint(90, `${count('ok')} fine (more than a year left); --all lists them.`));
|
|
127
|
+
console.log(paint(90, `Dates updated ${db.updated}. Sources: ${[db.eks.source, db.rds.source, db.lambda.source].join(' ')}`));
|
|
128
|
+
for (const w of warnings) console.error(paint(33, `warning: ${w}`));
|
|
129
|
+
}
|
|
130
|
+
return o['fail-on'] && failsAt(findings, o['fail-on']) ? 1 : 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (require.main === module) main().then((code) => { process.exitCode = code; });
|
|
134
|
+
module.exports = { main, scan, parseArgs, table };
|
package/src/radar.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Match what runs in an AWS account against retirements.yaml. Pure logic; the CLI does the I/O.
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const YAML = require('yaml');
|
|
5
|
+
|
|
6
|
+
const DAY = 86400000;
|
|
7
|
+
const SOON_DAYS = 90;
|
|
8
|
+
const UPCOMING_DAYS = 365;
|
|
9
|
+
// Worst first. "extended" = past standard support and paying for it (EKS, RDS).
|
|
10
|
+
const SEVERITY = ['retired', 'extended', 'soon', 'upcoming', 'ok', 'unknown'];
|
|
11
|
+
|
|
12
|
+
function loadRetirements(file = path.join(__dirname, '..', 'retirements.yaml')) {
|
|
13
|
+
return YAML.parse(fs.readFileSync(file, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// "13.12" -> "13", "9.6.24" -> "9.6", "8.0.35" -> "8.0", "5.7.mysql_aurora.2.11.2" -> "5.7"
|
|
17
|
+
function majorVersion(engine, version) {
|
|
18
|
+
const parts = String(version || '').split('.');
|
|
19
|
+
if (/postgres/.test(engine) && Number(parts[0]) >= 10) return parts[0];
|
|
20
|
+
return parts.slice(0, 2).join('.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const daysUntil = (date, now) => Math.ceil((Date.parse(date) - now) / DAY);
|
|
24
|
+
const byDays = (days) => (days <= SOON_DAYS ? 'soon' : days <= UPCOMING_DAYS ? 'upcoming' : 'ok');
|
|
25
|
+
|
|
26
|
+
// item: { service: 'EKS'|'RDS'|'Lambda', region, resource, version, engine? }
|
|
27
|
+
function evaluate(item, db, now = Date.now()) {
|
|
28
|
+
const f = { ...item, status: 'unknown', date: null, days: null, note: 'Version not in retirements.yaml', source: null };
|
|
29
|
+
if (item.service === 'EKS') {
|
|
30
|
+
const v = db.eks.versions[item.version];
|
|
31
|
+
f.source = db.eks.source;
|
|
32
|
+
if (!v) return f;
|
|
33
|
+
const std = daysUntil(v.standard, now);
|
|
34
|
+
const ext = daysUntil(v.extended, now);
|
|
35
|
+
if (ext < 0) return { ...f, status: 'retired', date: v.extended, days: ext, note: 'Extended support ended; AWS force-upgrades the control plane' };
|
|
36
|
+
if (std < 0) return { ...f, status: 'extended', date: v.extended, days: ext, note: `Paid extended support until this date, then AWS force-upgrades. ${db.eks.cost}`, monthlyCost: db.eks.monthlyCost };
|
|
37
|
+
return { ...f, status: byDays(std), date: v.standard, days: std, note: 'Standard support ends; extended support charges start' };
|
|
38
|
+
}
|
|
39
|
+
if (item.service === 'RDS') {
|
|
40
|
+
const major = majorVersion(item.engine, item.version);
|
|
41
|
+
const v = (db.rds.engines[item.engine] || {})[major];
|
|
42
|
+
f.source = db.rds.source;
|
|
43
|
+
if (!v) return f;
|
|
44
|
+
const std = daysUntil(v.standard, now);
|
|
45
|
+
if (std < 0) return { ...f, status: 'extended', date: v.standard, days: std, note: `Past standard support. ${db.rds.cost}` };
|
|
46
|
+
return { ...f, status: byDays(std), date: v.standard, days: std, note: 'Standard support ends; Extended Support billing starts' };
|
|
47
|
+
}
|
|
48
|
+
if (item.service === 'Lambda') {
|
|
49
|
+
const date = db.lambda.runtimes[item.version];
|
|
50
|
+
f.source = db.lambda.source;
|
|
51
|
+
if (!date) return f;
|
|
52
|
+
const d = daysUntil(date, now);
|
|
53
|
+
if (d < 0) return { ...f, status: 'retired', date, days: d, note: 'Runtime deprecated: no security patches' };
|
|
54
|
+
return { ...f, status: byDays(d), date, days: d, note: 'Runtime deprecation' };
|
|
55
|
+
}
|
|
56
|
+
return f;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const sortFindings = (list) => [...list].sort((a, b) =>
|
|
60
|
+
SEVERITY.indexOf(a.status) - SEVERITY.indexOf(b.status) || (a.days ?? 1e9) - (b.days ?? 1e9));
|
|
61
|
+
|
|
62
|
+
// Exit non-zero in CI when anything is at or worse than `level`.
|
|
63
|
+
const failsAt = (findings, level) => {
|
|
64
|
+
const max = SEVERITY.indexOf(level);
|
|
65
|
+
return max >= 0 && findings.some((f) => f.status !== 'unknown' && SEVERITY.indexOf(f.status) <= max);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
module.exports = { SEVERITY, loadRetirements, majorVersion, evaluate, sortFindings, failsAt };
|