@kevinrabun/judges 3.54.0 → 3.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +56 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/build-optimize.d.ts +7 -0
- package/dist/commands/build-optimize.d.ts.map +1 -0
- package/dist/commands/build-optimize.js +257 -0
- package/dist/commands/build-optimize.js.map +1 -0
- package/dist/commands/commit-hygiene.d.ts +6 -0
- package/dist/commands/commit-hygiene.d.ts.map +1 -0
- package/dist/commands/commit-hygiene.js +176 -0
- package/dist/commands/commit-hygiene.js.map +1 -0
- package/dist/commands/deploy-readiness.d.ts +6 -0
- package/dist/commands/deploy-readiness.d.ts.map +1 -0
- package/dist/commands/deploy-readiness.js +212 -0
- package/dist/commands/deploy-readiness.js.map +1 -0
- package/dist/commands/migration-safety.d.ts +6 -0
- package/dist/commands/migration-safety.d.ts.map +1 -0
- package/dist/commands/migration-safety.js +257 -0
- package/dist/commands/migration-safety.js.map +1 -0
- package/dist/commands/observability-gap.d.ts +6 -0
- package/dist/commands/observability-gap.d.ts.map +1 -0
- package/dist/commands/observability-gap.js +212 -0
- package/dist/commands/observability-gap.js.map +1 -0
- package/dist/commands/rollback-safety.d.ts +5 -0
- package/dist/commands/rollback-safety.d.ts.map +1 -0
- package/dist/commands/rollback-safety.js +192 -0
- package/dist/commands/rollback-safety.js.map +1 -0
- package/dist/commands/secret-age.d.ts +6 -0
- package/dist/commands/secret-age.d.ts.map +1 -0
- package/dist/commands/secret-age.js +215 -0
- package/dist/commands/secret-age.js.map +1 -0
- package/dist/commands/test-quality.d.ts +6 -0
- package/dist/commands/test-quality.d.ts.map +1 -0
- package/dist/commands/test-quality.js +161 -0
- package/dist/commands/test-quality.js.map +1 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability gap — detect missing metrics, traces, and structured logs
|
|
3
|
+
* at critical code paths.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
6
|
+
import { join, extname } from "path";
|
|
7
|
+
// ─── File Collection ────────────────────────────────────────────────────────
|
|
8
|
+
const CODE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", ".rs"]);
|
|
9
|
+
function collectFiles(dir, max = 300) {
|
|
10
|
+
const files = [];
|
|
11
|
+
function walk(d) {
|
|
12
|
+
if (files.length >= max)
|
|
13
|
+
return;
|
|
14
|
+
let entries;
|
|
15
|
+
try {
|
|
16
|
+
entries = readdirSync(d);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const e of entries) {
|
|
22
|
+
if (files.length >= max)
|
|
23
|
+
return;
|
|
24
|
+
if (e.startsWith(".") || e === "node_modules" || e === "dist" || e === "build")
|
|
25
|
+
continue;
|
|
26
|
+
const full = join(d, e);
|
|
27
|
+
try {
|
|
28
|
+
if (statSync(full).isDirectory())
|
|
29
|
+
walk(full);
|
|
30
|
+
else if (CODE_EXTS.has(extname(full)))
|
|
31
|
+
files.push(full);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* skip */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
walk(dir);
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
// ─── Analysis ───────────────────────────────────────────────────────────────
|
|
42
|
+
function analyzeFile(filepath) {
|
|
43
|
+
const gaps = [];
|
|
44
|
+
let content;
|
|
45
|
+
try {
|
|
46
|
+
content = readFileSync(filepath, "utf-8");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return gaps;
|
|
50
|
+
}
|
|
51
|
+
const lines = content.split("\n");
|
|
52
|
+
// Track context for scoped analysis
|
|
53
|
+
let inCatchBlock = false;
|
|
54
|
+
let catchStart = 0;
|
|
55
|
+
let inRouteHandler = false;
|
|
56
|
+
let routeStart = 0;
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
const line = lines[i];
|
|
59
|
+
// Detect catch blocks
|
|
60
|
+
if (/\bcatch\s*\(/.test(line)) {
|
|
61
|
+
inCatchBlock = true;
|
|
62
|
+
catchStart = i;
|
|
63
|
+
}
|
|
64
|
+
if (inCatchBlock && i - catchStart > 10)
|
|
65
|
+
inCatchBlock = false;
|
|
66
|
+
// Catch without logging
|
|
67
|
+
if (inCatchBlock && /\bcatch\s*\(/.test(line)) {
|
|
68
|
+
const block = lines.slice(i, Math.min(i + 8, lines.length)).join("\n");
|
|
69
|
+
if (!/log|logger|console\.(error|warn|log)|logging|slog|zerolog|println|print/i.test(block)) {
|
|
70
|
+
gaps.push({
|
|
71
|
+
file: filepath,
|
|
72
|
+
line: i + 1,
|
|
73
|
+
gap: "Silent catch block",
|
|
74
|
+
severity: "high",
|
|
75
|
+
suggestion: "Log error details with context (request ID, user, operation)",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Detect route/endpoint handlers
|
|
80
|
+
if (/\.(get|post|put|delete|patch)\s*\(\s*['"]\//.test(line) ||
|
|
81
|
+
/@(Get|Post|Put|Delete|Patch)Mapping/.test(line) ||
|
|
82
|
+
/app\.route|@app\.(get|post|put|delete)/.test(line)) {
|
|
83
|
+
inRouteHandler = true;
|
|
84
|
+
routeStart = i;
|
|
85
|
+
}
|
|
86
|
+
if (inRouteHandler && i - routeStart > 30)
|
|
87
|
+
inRouteHandler = false;
|
|
88
|
+
// HTTP handler without latency tracking
|
|
89
|
+
if (inRouteHandler && i === routeStart) {
|
|
90
|
+
const block = lines.slice(i, Math.min(i + 30, lines.length)).join("\n");
|
|
91
|
+
if (!/histogram|timer|latency|duration|perf_hooks|performance\.now|time\.Since|Stopwatch/i.test(block)) {
|
|
92
|
+
gaps.push({
|
|
93
|
+
file: filepath,
|
|
94
|
+
line: i + 1,
|
|
95
|
+
gap: "Endpoint without latency metrics",
|
|
96
|
+
severity: "medium",
|
|
97
|
+
suggestion: "Add response-time histogram/metric for SLO monitoring",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// External calls without tracing
|
|
102
|
+
if (/fetch\(|axios\.|http\.request|httpClient|requests\.(get|post)|gorequest|reqwest/i.test(line)) {
|
|
103
|
+
const surrounding = lines.slice(Math.max(0, i - 3), Math.min(i + 3, lines.length)).join("\n");
|
|
104
|
+
if (!/trace|span|opentelemetry|tracing|X-Request-Id|correlation.?id|traceparent/i.test(surrounding)) {
|
|
105
|
+
gaps.push({
|
|
106
|
+
file: filepath,
|
|
107
|
+
line: i + 1,
|
|
108
|
+
gap: "Outbound call without trace propagation",
|
|
109
|
+
severity: "medium",
|
|
110
|
+
suggestion: "Propagate trace context (traceparent header) for distributed tracing",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Background/scheduled jobs without heartbeat
|
|
115
|
+
if (/setInterval|cron\.schedule|@Scheduled|BackgroundJob|celery|sidekiq/i.test(line)) {
|
|
116
|
+
const surrounding = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
117
|
+
if (!/heartbeat|health|alive|lastRun|metric|gauge/i.test(surrounding)) {
|
|
118
|
+
gaps.push({
|
|
119
|
+
file: filepath,
|
|
120
|
+
line: i + 1,
|
|
121
|
+
gap: "Background job without heartbeat",
|
|
122
|
+
severity: "high",
|
|
123
|
+
suggestion: "Emit heartbeat metric or last-run timestamp for alerting",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Database queries without instrumentation
|
|
128
|
+
if (/\.query\s*\(|\.execute\s*\(|\.raw\s*\(|cursor\.execute|db\.Exec|sqlx/i.test(line)) {
|
|
129
|
+
const surrounding = lines.slice(Math.max(0, i - 2), Math.min(i + 5, lines.length)).join("\n");
|
|
130
|
+
if (!/span|trace|metric|histogram|timer|duration|slow.?query/i.test(surrounding)) {
|
|
131
|
+
gaps.push({
|
|
132
|
+
file: filepath,
|
|
133
|
+
line: i + 1,
|
|
134
|
+
gap: "DB query without instrumentation",
|
|
135
|
+
severity: "low",
|
|
136
|
+
suggestion: "Add query timing to detect slow queries",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Queue/message consumers without metrics
|
|
141
|
+
if (/\.consume\s*\(|\.subscribe\s*\(|@RabbitListener|@KafkaListener|on\s*\(\s*['"]message['"]\s*\)/i.test(line)) {
|
|
142
|
+
const surrounding = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
143
|
+
if (!/metric|counter|gauge|processed|lag|backlog/i.test(surrounding)) {
|
|
144
|
+
gaps.push({
|
|
145
|
+
file: filepath,
|
|
146
|
+
line: i + 1,
|
|
147
|
+
gap: "Message consumer without processing metrics",
|
|
148
|
+
severity: "medium",
|
|
149
|
+
suggestion: "Track message processing rate, lag, and error counters",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return gaps;
|
|
155
|
+
}
|
|
156
|
+
// ─── CLI ────────────────────────────────────────────────────────────────────
|
|
157
|
+
export function runObservabilityGap(argv) {
|
|
158
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
159
|
+
console.log(`
|
|
160
|
+
judges observability-gap — Detect missing instrumentation at critical paths
|
|
161
|
+
|
|
162
|
+
Usage:
|
|
163
|
+
judges observability-gap [dir]
|
|
164
|
+
judges observability-gap src/ --format json
|
|
165
|
+
|
|
166
|
+
Options:
|
|
167
|
+
[dir] Directory to scan (default: .)
|
|
168
|
+
--format json JSON output
|
|
169
|
+
--help, -h Show this help
|
|
170
|
+
|
|
171
|
+
Detects: silent catch blocks, endpoints without latency metrics, outbound calls
|
|
172
|
+
without trace propagation, background jobs without heartbeats, DB queries without
|
|
173
|
+
instrumentation, message consumers without processing metrics.
|
|
174
|
+
`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const format = argv.find((_a, i) => argv[i - 1] === "--format") || "text";
|
|
178
|
+
const dir = argv.find((a) => !a.startsWith("-") && argv.indexOf(a) > 0) || ".";
|
|
179
|
+
const files = collectFiles(dir);
|
|
180
|
+
const allGaps = [];
|
|
181
|
+
for (const f of files)
|
|
182
|
+
allGaps.push(...analyzeFile(f));
|
|
183
|
+
const highCount = allGaps.filter((g) => g.severity === "high").length;
|
|
184
|
+
const medCount = allGaps.filter((g) => g.severity === "medium").length;
|
|
185
|
+
const score = Math.max(0, 100 - highCount * 10 - medCount * 5);
|
|
186
|
+
if (format === "json") {
|
|
187
|
+
console.log(JSON.stringify({
|
|
188
|
+
gaps: allGaps,
|
|
189
|
+
score,
|
|
190
|
+
summary: { high: highCount, medium: medCount, total: allGaps.length },
|
|
191
|
+
timestamp: new Date().toISOString(),
|
|
192
|
+
}, null, 2));
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const badge = score >= 80 ? "✅ WELL INSTRUMENTED" : score >= 50 ? "⚠️ GAPS FOUND" : "❌ BLIND SPOTS";
|
|
196
|
+
console.log(`\n Observability: ${badge} (${score}/100)\n ──────────────────────────────`);
|
|
197
|
+
if (allGaps.length === 0) {
|
|
198
|
+
console.log(" No observability gaps detected.\n");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
for (const g of allGaps.slice(0, 25)) {
|
|
202
|
+
const icon = g.severity === "high" ? "🔴" : g.severity === "medium" ? "🟡" : "🔵";
|
|
203
|
+
console.log(` ${icon} ${g.gap}`);
|
|
204
|
+
console.log(` ${g.file}:${g.line}`);
|
|
205
|
+
console.log(` → ${g.suggestion}`);
|
|
206
|
+
}
|
|
207
|
+
if (allGaps.length > 25)
|
|
208
|
+
console.log(` ... and ${allGaps.length - 25} more`);
|
|
209
|
+
console.log(`\n Total: ${allGaps.length} | High: ${highCount} | Medium: ${medCount} | Score: ${score}/100\n`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=observability-gap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability-gap.js","sourceRoot":"","sources":["../../src/commands/observability-gap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rollback-safety.d.ts","sourceRoot":"","sources":["../../src/commands/rollback-safety.ts"],"names":[],"mappings":"AAAA;;GAEG;AAiJH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAqEtD"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rollback safety — detect changes that are unsafe or impossible to roll back.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
5
|
+
import { join, extname } from "path";
|
|
6
|
+
// ─── File Collection ────────────────────────────────────────────────────────
|
|
7
|
+
const CODE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", ".rs", ".sql", ".yaml", ".yml"]);
|
|
8
|
+
function collectFiles(dir, max = 300) {
|
|
9
|
+
const files = [];
|
|
10
|
+
function walk(d) {
|
|
11
|
+
if (files.length >= max)
|
|
12
|
+
return;
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = readdirSync(d);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const e of entries) {
|
|
21
|
+
if (files.length >= max)
|
|
22
|
+
return;
|
|
23
|
+
if (e.startsWith(".") || e === "node_modules" || e === "dist" || e === "build")
|
|
24
|
+
continue;
|
|
25
|
+
const full = join(d, e);
|
|
26
|
+
try {
|
|
27
|
+
if (statSync(full).isDirectory())
|
|
28
|
+
walk(full);
|
|
29
|
+
else if (CODE_EXTS.has(extname(full)))
|
|
30
|
+
files.push(full);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* skip */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
walk(dir);
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
// ─── Analysis ───────────────────────────────────────────────────────────────
|
|
41
|
+
const RISK_PATTERNS = [
|
|
42
|
+
{
|
|
43
|
+
pattern: /DROP\s+TABLE/i,
|
|
44
|
+
risk: "Destructive migration",
|
|
45
|
+
severity: "critical",
|
|
46
|
+
detail: "Drops entire table — data unrecoverable without backup",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
pattern: /DROP\s+COLUMN/i,
|
|
50
|
+
risk: "Column removal",
|
|
51
|
+
severity: "critical",
|
|
52
|
+
detail: "Dropped column data lost — add deprecation period first",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
pattern: /ALTER\s+TABLE.*RENAME/i,
|
|
56
|
+
risk: "Table/column rename",
|
|
57
|
+
severity: "high",
|
|
58
|
+
detail: "Rename breaks old queries — use alias during transition",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
pattern: /TRUNCATE\s+/i,
|
|
62
|
+
risk: "Table truncation",
|
|
63
|
+
severity: "critical",
|
|
64
|
+
detail: "Removes all rows — cannot undo without backup",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
pattern: /DELETE\s+FROM\s+\w+\s*(?:;|$)/im,
|
|
68
|
+
risk: "Mass delete",
|
|
69
|
+
severity: "high",
|
|
70
|
+
detail: "Delete without WHERE — removes all rows",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
pattern: /ALTER\s+TYPE|CHANGE\s+COLUMN.*\bTYPE\b/i,
|
|
74
|
+
risk: "Column type change",
|
|
75
|
+
severity: "high",
|
|
76
|
+
detail: "Type narrowing can lose data silently",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
pattern: /removeField|removeColumn|dropIndex/i,
|
|
80
|
+
risk: "ORM schema removal",
|
|
81
|
+
severity: "high",
|
|
82
|
+
detail: "Field/index removal in ORM migration — deploy new code first",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
pattern: /\.destroy\(\)|\.deleteMany\(\{?\s*\}?\)|\.remove\(\{?\s*\}?\)/i,
|
|
86
|
+
risk: "Bulk data deletion",
|
|
87
|
+
severity: "high",
|
|
88
|
+
detail: "Bulk delete in application code — ensure filters are correct",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
pattern: /(?:api|endpoint|route).*(?:removed|deprecated|deleted)/i,
|
|
92
|
+
risk: "API endpoint removal",
|
|
93
|
+
severity: "high",
|
|
94
|
+
detail: "Removing endpoints breaks consumers — deprecate first",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
pattern: /(?:encryption|crypto).*(?:changed|migrated|switched)/i,
|
|
98
|
+
risk: "Encryption scheme change",
|
|
99
|
+
severity: "critical",
|
|
100
|
+
detail: "Changing encryption makes old data unreadable — migrate gradually",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
pattern: /(?:partition|shard).*(?:key|strategy).*(?:change|update)/i,
|
|
104
|
+
risk: "Partition key change",
|
|
105
|
+
severity: "critical",
|
|
106
|
+
detail: "Partition key change requires full data re-distribution",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
pattern: /irreversible|no.?rollback|one.?way/i,
|
|
110
|
+
risk: "Explicit irreversibility marker",
|
|
111
|
+
severity: "high",
|
|
112
|
+
detail: "Code explicitly marked as irreversible",
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
function analyzeFile(filepath) {
|
|
116
|
+
const risks = [];
|
|
117
|
+
let content;
|
|
118
|
+
try {
|
|
119
|
+
content = readFileSync(filepath, "utf-8");
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return risks;
|
|
123
|
+
}
|
|
124
|
+
const lines = content.split("\n");
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
const line = lines[i];
|
|
127
|
+
for (const rp of RISK_PATTERNS) {
|
|
128
|
+
if (rp.pattern.test(line)) {
|
|
129
|
+
risks.push({ file: filepath, line: i + 1, risk: rp.risk, severity: rp.severity, detail: rp.detail });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return risks;
|
|
134
|
+
}
|
|
135
|
+
// ─── CLI ────────────────────────────────────────────────────────────────────
|
|
136
|
+
export function runRollbackSafety(argv) {
|
|
137
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
138
|
+
console.log(`
|
|
139
|
+
judges rollback-safety — Detect changes unsafe or impossible to roll back
|
|
140
|
+
|
|
141
|
+
Usage:
|
|
142
|
+
judges rollback-safety [dir]
|
|
143
|
+
judges rollback-safety migrations/ --format json
|
|
144
|
+
|
|
145
|
+
Options:
|
|
146
|
+
[dir] Directory to scan (default: .)
|
|
147
|
+
--format json JSON output
|
|
148
|
+
--help, -h Show this help
|
|
149
|
+
|
|
150
|
+
Detects: destructive DB migrations, bulk deletes, API removals, encryption changes,
|
|
151
|
+
partition key changes, and code explicitly marked irreversible.
|
|
152
|
+
`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const format = argv.find((_a, i) => argv[i - 1] === "--format") || "text";
|
|
156
|
+
const dir = argv.find((a) => !a.startsWith("-") && argv.indexOf(a) > 0) || ".";
|
|
157
|
+
const files = collectFiles(dir);
|
|
158
|
+
const risks = [];
|
|
159
|
+
for (const f of files)
|
|
160
|
+
risks.push(...analyzeFile(f));
|
|
161
|
+
risks.sort((a, b) => {
|
|
162
|
+
const order = { critical: 0, high: 1, medium: 2 };
|
|
163
|
+
return order[a.severity] - order[b.severity];
|
|
164
|
+
});
|
|
165
|
+
const critCount = risks.filter((r) => r.severity === "critical").length;
|
|
166
|
+
const highCount = risks.filter((r) => r.severity === "high").length;
|
|
167
|
+
const score = risks.length === 0 ? 100 : Math.max(0, 100 - critCount * 25 - highCount * 10);
|
|
168
|
+
if (format === "json") {
|
|
169
|
+
console.log(JSON.stringify({
|
|
170
|
+
risks,
|
|
171
|
+
score,
|
|
172
|
+
summary: { critical: critCount, high: highCount, total: risks.length },
|
|
173
|
+
timestamp: new Date().toISOString(),
|
|
174
|
+
}, null, 2));
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const badge = critCount > 0 ? "🚫 UNSAFE" : highCount > 0 ? "⚠️ CAUTION" : "✅ SAFE";
|
|
178
|
+
console.log(`\n Rollback Safety: ${badge} (score ${score}/100)\n ─────────────────────────`);
|
|
179
|
+
if (risks.length === 0) {
|
|
180
|
+
console.log(" No rollback risks detected.\n");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
for (const r of risks) {
|
|
184
|
+
const icon = r.severity === "critical" ? "🚫" : r.severity === "high" ? "🔴" : "🟡";
|
|
185
|
+
console.log(` ${icon} [${r.severity.toUpperCase()}] ${r.risk}`);
|
|
186
|
+
console.log(` ${r.file}:${r.line}`);
|
|
187
|
+
console.log(` ${r.detail}`);
|
|
188
|
+
}
|
|
189
|
+
console.log(`\n Total: ${risks.length} risks | Critical: ${critCount} | High: ${highCount} | Score: ${score}/100\n`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=rollback-safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rollback-safety.js","sourceRoot":"","sources":["../../src/commands/rollback-safety.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-age.d.ts","sourceRoot":"","sources":["../../src/commands/secret-age.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAkLH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAiEjD"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret age — detect credentials with no rotation policy, hardcoded expiry,
|
|
3
|
+
* missing vault references, and shared service-account credentials.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
6
|
+
import { join, extname } from "path";
|
|
7
|
+
// ─── File Collection ────────────────────────────────────────────────────────
|
|
8
|
+
const SCAN_EXTS = new Set([
|
|
9
|
+
".ts",
|
|
10
|
+
".tsx",
|
|
11
|
+
".js",
|
|
12
|
+
".jsx",
|
|
13
|
+
".py",
|
|
14
|
+
".java",
|
|
15
|
+
".go",
|
|
16
|
+
".rs",
|
|
17
|
+
".yaml",
|
|
18
|
+
".yml",
|
|
19
|
+
".json",
|
|
20
|
+
".env",
|
|
21
|
+
".toml",
|
|
22
|
+
".cfg",
|
|
23
|
+
".ini",
|
|
24
|
+
".xml",
|
|
25
|
+
]);
|
|
26
|
+
function collectFiles(dir, max = 400) {
|
|
27
|
+
const files = [];
|
|
28
|
+
function walk(d) {
|
|
29
|
+
if (files.length >= max)
|
|
30
|
+
return;
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = readdirSync(d);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
if (files.length >= max)
|
|
40
|
+
return;
|
|
41
|
+
if (e.startsWith(".") || e === "node_modules" || e === "dist" || e === "build")
|
|
42
|
+
continue;
|
|
43
|
+
const full = join(d, e);
|
|
44
|
+
try {
|
|
45
|
+
if (statSync(full).isDirectory())
|
|
46
|
+
walk(full);
|
|
47
|
+
else if (SCAN_EXTS.has(extname(full)))
|
|
48
|
+
files.push(full);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* skip */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
walk(dir);
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
// ─── Patterns ───────────────────────────────────────────────────────────────
|
|
59
|
+
const SECRET_PATTERNS = [
|
|
60
|
+
{
|
|
61
|
+
pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][A-Za-z0-9]{16,}['"]/i,
|
|
62
|
+
finding: "Hardcoded API key",
|
|
63
|
+
severity: "critical",
|
|
64
|
+
recommendation: "Move to vault or environment variable",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}['"]/i,
|
|
68
|
+
finding: "Hardcoded password",
|
|
69
|
+
severity: "critical",
|
|
70
|
+
recommendation: "Use a secrets manager",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
pattern: /(?:secret|token)\s*[:=]\s*['"][A-Za-z0-9+/=]{16,}['"]/i,
|
|
74
|
+
finding: "Hardcoded secret/token",
|
|
75
|
+
severity: "critical",
|
|
76
|
+
recommendation: "Store in vault (AWS SSM, Azure Key Vault, HashiCorp Vault)",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
pattern: /(?:private[_-]?key|privateKey)\s*[:=]\s*['"`]-----BEGIN/i,
|
|
80
|
+
finding: "Embedded private key",
|
|
81
|
+
severity: "critical",
|
|
82
|
+
recommendation: "Store private keys in a secure key store",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
pattern: /(?:connection[_-]?string|connStr)\s*[:=]\s*['"][^'"]{20,}['"]/i,
|
|
86
|
+
finding: "Hardcoded connection string",
|
|
87
|
+
severity: "high",
|
|
88
|
+
recommendation: "Reference from vault; use managed identity where possible",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
pattern: /expires?\s*[:=]\s*['"]?\d{4}[-/]\d{2}[-/]\d{2}/i,
|
|
92
|
+
finding: "Hardcoded expiry date",
|
|
93
|
+
severity: "medium",
|
|
94
|
+
recommendation: "Implement dynamic rotation; avoid static expiry",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
pattern: /rotation|rotate.*(?:never|disabled|false)/i,
|
|
98
|
+
finding: "Rotation disabled",
|
|
99
|
+
severity: "high",
|
|
100
|
+
recommendation: "Enable automatic credential rotation",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
pattern: /(?:aws_access_key_id|aws_secret_access_key)\s*[:=]\s*['"][A-Z0-9]{16,}['"]/i,
|
|
104
|
+
finding: "AWS credentials in source",
|
|
105
|
+
severity: "critical",
|
|
106
|
+
recommendation: "Use IAM roles or environment credentials",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
pattern: /(?:GOOGLE_APPLICATION_CREDENTIALS|gcp_credentials)\s*[:=]\s*['"][^'"]+['"]/i,
|
|
110
|
+
finding: "GCP credentials in source",
|
|
111
|
+
severity: "high",
|
|
112
|
+
recommendation: "Use workload identity or service account key file outside repo",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
pattern: /(?:shared|common)[_-]?(?:service)?[_-]?(?:account|credential|key)/i,
|
|
116
|
+
finding: "Shared service credentials",
|
|
117
|
+
severity: "high",
|
|
118
|
+
recommendation: "Use per-environment, per-service credentials",
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
function analyzeFile(filepath) {
|
|
122
|
+
const findings = [];
|
|
123
|
+
let content;
|
|
124
|
+
try {
|
|
125
|
+
content = readFileSync(filepath, "utf-8");
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return findings;
|
|
129
|
+
}
|
|
130
|
+
const lines = content.split("\n");
|
|
131
|
+
for (let i = 0; i < lines.length; i++) {
|
|
132
|
+
const line = lines[i];
|
|
133
|
+
// Skip comments
|
|
134
|
+
if (/^\s*(?:\/\/|#|\/\*|\*)/.test(line))
|
|
135
|
+
continue;
|
|
136
|
+
for (const sp of SECRET_PATTERNS) {
|
|
137
|
+
if (sp.pattern.test(line)) {
|
|
138
|
+
findings.push({
|
|
139
|
+
file: filepath,
|
|
140
|
+
line: i + 1,
|
|
141
|
+
finding: sp.finding,
|
|
142
|
+
severity: sp.severity,
|
|
143
|
+
recommendation: sp.recommendation,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Check for vault integration
|
|
149
|
+
const hasVaultRef = /vault|keyVault|ssm|secretsmanager|VAULT_ADDR|SecretClient/i.test(content);
|
|
150
|
+
const hasSecrets = findings.length > 0;
|
|
151
|
+
if (hasSecrets && !hasVaultRef) {
|
|
152
|
+
findings.push({
|
|
153
|
+
file: filepath,
|
|
154
|
+
line: 1,
|
|
155
|
+
finding: "No vault integration detected",
|
|
156
|
+
severity: "medium",
|
|
157
|
+
recommendation: "Add a secrets manager reference for credential lifecycle management",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return findings;
|
|
161
|
+
}
|
|
162
|
+
// ─── CLI ────────────────────────────────────────────────────────────────────
|
|
163
|
+
export function runSecretAge(argv) {
|
|
164
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
165
|
+
console.log(`
|
|
166
|
+
judges secret-age — Credential lifecycle and rotation analysis
|
|
167
|
+
|
|
168
|
+
Usage:
|
|
169
|
+
judges secret-age [dir]
|
|
170
|
+
judges secret-age src/ --format json
|
|
171
|
+
|
|
172
|
+
Options:
|
|
173
|
+
[dir] Directory to scan (default: .)
|
|
174
|
+
--format json JSON output
|
|
175
|
+
--help, -h Show this help
|
|
176
|
+
|
|
177
|
+
Detects: hardcoded API keys, passwords, tokens, private keys, connection strings,
|
|
178
|
+
disabled rotation, hardcoded expiry, shared credentials, missing vault integration.
|
|
179
|
+
`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const format = argv.find((_a, i) => argv[i - 1] === "--format") || "text";
|
|
183
|
+
const dir = argv.find((a) => !a.startsWith("-") && argv.indexOf(a) > 0) || ".";
|
|
184
|
+
const files = collectFiles(dir);
|
|
185
|
+
const allFindings = [];
|
|
186
|
+
for (const f of files)
|
|
187
|
+
allFindings.push(...analyzeFile(f));
|
|
188
|
+
const critCount = allFindings.filter((f) => f.severity === "critical").length;
|
|
189
|
+
const highCount = allFindings.filter((f) => f.severity === "high").length;
|
|
190
|
+
const score = allFindings.length === 0 ? 100 : Math.max(0, 100 - critCount * 20 - highCount * 10);
|
|
191
|
+
if (format === "json") {
|
|
192
|
+
console.log(JSON.stringify({
|
|
193
|
+
findings: allFindings,
|
|
194
|
+
score,
|
|
195
|
+
summary: { critical: critCount, high: highCount, total: allFindings.length },
|
|
196
|
+
timestamp: new Date().toISOString(),
|
|
197
|
+
}, null, 2));
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const badge = critCount > 0 ? "🚫 EXPOSED" : highCount > 0 ? "⚠️ AT RISK" : allFindings.length > 0 ? "🟡 REVIEW" : "✅ CLEAN";
|
|
201
|
+
console.log(`\n Secret Age Analysis: ${badge} (${score}/100)\n ─────────────────────────────`);
|
|
202
|
+
if (allFindings.length === 0) {
|
|
203
|
+
console.log(" No credential lifecycle issues detected.\n");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
for (const f of allFindings) {
|
|
207
|
+
const icon = f.severity === "critical" ? "🚫" : f.severity === "high" ? "🔴" : "🟡";
|
|
208
|
+
console.log(` ${icon} [${f.severity.toUpperCase()}] ${f.finding}`);
|
|
209
|
+
console.log(` ${f.file}:${f.line}`);
|
|
210
|
+
console.log(` → ${f.recommendation}`);
|
|
211
|
+
}
|
|
212
|
+
console.log(`\n Total: ${allFindings.length} | Critical: ${critCount} | High: ${highCount} | Score: ${score}/100\n`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=secret-age.js.map
|