mcp-spring-boot-actuator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/build/analyzers/beans.js +179 -0
- package/build/analyzers/caches.js +84 -0
- package/build/analyzers/env-risk.js +165 -0
- package/build/analyzers/loggers.js +133 -0
- package/build/analyzers/metrics.js +201 -0
- package/build/analyzers/startup.js +136 -0
- package/build/index.js +337 -0
- package/build/license.js +115 -0
- package/build/parsers/health.js +180 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmytro Lisnichenko
|
|
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,206 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/mcp-spring-boot-actuator)
|
|
2
|
+
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
|
|
4
|
+
# MCP Spring Boot Actuator
|
|
5
|
+
|
|
6
|
+
An MCP server that analyzes Spring Boot Actuator endpoints — health, metrics, environment, beans, startup, and caches. Detects issues, security risks, and provides actionable recommendations.
|
|
7
|
+
|
|
8
|
+
## Why This Tool?
|
|
9
|
+
|
|
10
|
+
There is **no other MCP server** that analyzes Spring Boot Actuator endpoints. This is the only tool that lets your AI assistant understand your Spring Boot application's health, performance, configuration, and startup behavior through actuator data.
|
|
11
|
+
|
|
12
|
+
7 analytical tools turn raw actuator JSON into actionable diagnostics — health checks, JVM metrics analysis, security risk detection in environment/beans, startup bottleneck identification, and cache efficiency analysis.
|
|
13
|
+
|
|
14
|
+
## Tools (7)
|
|
15
|
+
|
|
16
|
+
### `analyze_health`
|
|
17
|
+
|
|
18
|
+
Parse and diagnose the `/health` endpoint response. Detects unhealthy components (database, Redis, Kafka, Elasticsearch, disk space) with component-specific recommendations.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
curl http://localhost:8080/actuator/health | jq '.' > health.json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Detects:**
|
|
25
|
+
- DOWN/OUT_OF_SERVICE components
|
|
26
|
+
- Low disk space warnings (< 15% free)
|
|
27
|
+
- Nested component health (e.g., primary/secondary databases)
|
|
28
|
+
- Restricted health endpoints (no `show-details`)
|
|
29
|
+
|
|
30
|
+
### `analyze_metrics`
|
|
31
|
+
|
|
32
|
+
Analyze JVM, HTTP, and database pool metrics from `/metrics` endpoints.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
# Collect metrics into a single JSON object:
|
|
36
|
+
{
|
|
37
|
+
"jvm.memory.used": 800000000,
|
|
38
|
+
"jvm.memory.max": 1000000000,
|
|
39
|
+
"jvm.threads.live": 150,
|
|
40
|
+
"jvm.gc.pause.count": 500,
|
|
41
|
+
"jvm.gc.pause.total": 8.5,
|
|
42
|
+
"http.server.requests.count": 10000,
|
|
43
|
+
"http.server.requests.error.count": 50,
|
|
44
|
+
"hikaricp.connections.active": 8,
|
|
45
|
+
"hikaricp.connections.max": 10
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Detects:**
|
|
50
|
+
- Heap utilization >= 90% (CRITICAL) or > 75% (WARNING)
|
|
51
|
+
- High thread count (> 500)
|
|
52
|
+
- Long GC pauses (avg > 200ms)
|
|
53
|
+
- HTTP error rate > 10% (CRITICAL) or > 1% (WARNING)
|
|
54
|
+
- Connection pool exhaustion >= 90% (CRITICAL)
|
|
55
|
+
- Pending connection requests
|
|
56
|
+
|
|
57
|
+
Supports both flat metric values and Spring Boot measurement format (`{ measurements: [{ statistic: "VALUE", value: N }] }`).
|
|
58
|
+
|
|
59
|
+
### `analyze_env`
|
|
60
|
+
|
|
61
|
+
Analyze the `/env` endpoint for security risks and misconfigurations.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
curl http://localhost:8080/actuator/env | jq '.' > env.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Detects:**
|
|
68
|
+
- Exposed secrets (passwords, API keys, tokens not masked with `******`)
|
|
69
|
+
- Risky production configs: `ddl-auto: create-drop`, H2 console enabled, `show-sql: true`
|
|
70
|
+
- DevTools enabled in production
|
|
71
|
+
- All actuator endpoints exposed (`management.endpoints.web.exposure.include=*`)
|
|
72
|
+
- Missing Spring profiles (no active profiles set)
|
|
73
|
+
|
|
74
|
+
### `analyze_beans`
|
|
75
|
+
|
|
76
|
+
Analyze the `/beans` endpoint for architectural issues.
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
curl http://localhost:8080/actuator/beans | jq '.' > beans.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Detects:**
|
|
83
|
+
- Circular dependencies (A → B → A)
|
|
84
|
+
- Singleton beans depending on prototype-scoped beans (scope mismatch)
|
|
85
|
+
- Beans with > 10 dependencies (God objects)
|
|
86
|
+
- Large bean counts (> 500)
|
|
87
|
+
- Multiple application contexts
|
|
88
|
+
|
|
89
|
+
### `analyze_startup`
|
|
90
|
+
|
|
91
|
+
Analyze the `/startup` actuator endpoint (Spring Boot 3.2+). Parses the startup timeline to detect slow bean initialization and heavy auto-configurations.
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
curl -X POST http://localhost:8080/actuator/startup | jq '.' > startup.json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Parameters:**
|
|
98
|
+
- `json` — The `/startup` endpoint JSON response
|
|
99
|
+
|
|
100
|
+
**Detects:**
|
|
101
|
+
- Slow startup (> 30s CRITICAL, > 15s WARNING)
|
|
102
|
+
- Heavy auto-configurations consuming > 30% of startup time
|
|
103
|
+
- Slow bean initialization (> 2s per bean)
|
|
104
|
+
- Top slowest steps ranked by duration
|
|
105
|
+
|
|
106
|
+
### `analyze_caches`
|
|
107
|
+
|
|
108
|
+
Analyze the `/caches` actuator endpoint. Lists registered caches and detects configuration issues.
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
curl http://localhost:8080/actuator/caches | jq '.' > caches.json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Parameters:**
|
|
115
|
+
- `json` — The `/caches` endpoint JSON response
|
|
116
|
+
|
|
117
|
+
**Detects:**
|
|
118
|
+
- Unbounded `ConcurrentMapCache` usage (no eviction, will grow indefinitely)
|
|
119
|
+
- Too many caches (> 20, memory overhead)
|
|
120
|
+
- Missing cache managers (no Spring Cache configured)
|
|
121
|
+
- Empty cache registrations
|
|
122
|
+
|
|
123
|
+
### `analyze_loggers`
|
|
124
|
+
|
|
125
|
+
Analyze the `/loggers` actuator endpoint. Detects verbose logging configurations that impact performance and security in production.
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
curl http://localhost:8080/actuator/loggers | jq '.' > loggers.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Parameters:**
|
|
132
|
+
- `json` — The `/loggers` endpoint JSON response
|
|
133
|
+
|
|
134
|
+
**Detects:**
|
|
135
|
+
- ROOT logger set to DEBUG/TRACE (floods logs, degrades performance)
|
|
136
|
+
- Explicitly configured DEBUG/TRACE loggers (likely leftover from debugging)
|
|
137
|
+
- Verbose framework logging (Spring, Hibernate, HikariCP, Micrometer, Apache)
|
|
138
|
+
- Inconsistent log levels across related packages
|
|
139
|
+
- More than 5 verbose loggers (signs of leftover debug configuration)
|
|
140
|
+
|
|
141
|
+
## Installation
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm install -g mcp-spring-boot-actuator
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Or use directly with npx:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx mcp-spring-boot-actuator
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Configuration
|
|
154
|
+
|
|
155
|
+
### Claude Desktop
|
|
156
|
+
|
|
157
|
+
Add to your Claude Desktop config (`~/.claude/claude_desktop_config.json`):
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"mcpServers": {
|
|
162
|
+
"spring-boot-actuator": {
|
|
163
|
+
"command": "npx",
|
|
164
|
+
"args": ["-y", "mcp-spring-boot-actuator"]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Quick Demo
|
|
171
|
+
|
|
172
|
+
Once configured, try these prompts in Claude:
|
|
173
|
+
|
|
174
|
+
1. **"Check the health of my Spring Boot app: [paste /actuator/health JSON]"** — Detects DOWN components, low disk space, and provides component-specific recommendations
|
|
175
|
+
2. **"Are there any security risks in my config? [paste /actuator/env JSON]"** — Finds exposed secrets, risky settings like `ddl-auto: create-drop`, and over-exposed endpoints
|
|
176
|
+
3. **"How is my app performing? [paste JVM/HTTP metrics]"** — Analyzes heap usage, GC pressure, HTTP error rates, and connection pool utilization
|
|
177
|
+
- "Why is my app starting so slowly?" (paste `/actuator/startup` JSON)
|
|
178
|
+
- "Are my caches configured properly?" (paste `/actuator/caches` JSON)
|
|
179
|
+
|
|
180
|
+
## Requirements
|
|
181
|
+
|
|
182
|
+
- Node.js 18+
|
|
183
|
+
- Spring Boot application with Actuator endpoints enabled
|
|
184
|
+
|
|
185
|
+
## Part of the MCP Java Backend Suite
|
|
186
|
+
|
|
187
|
+
This server works alongside:
|
|
188
|
+
- [mcp-db-analyzer](https://www.npmjs.com/package/mcp-db-analyzer) — Database performance analysis
|
|
189
|
+
- [mcp-jvm-diagnostics](https://www.npmjs.com/package/mcp-jvm-diagnostics) — Thread dump and GC log analysis
|
|
190
|
+
- [mcp-migration-advisor](https://www.npmjs.com/package/mcp-migration-advisor) — Schema migration risk analysis
|
|
191
|
+
|
|
192
|
+
## Limitations & Known Issues
|
|
193
|
+
|
|
194
|
+
- **Actuator endpoints must be exposed**: Spring Boot secures actuator endpoints by default. You must explicitly expose endpoints via `management.endpoints.web.exposure.include`.
|
|
195
|
+
- **Startup analysis**: Requires Spring Boot 3.2+ with `management.endpoint.startup.enabled=true`. Older versions don't provide startup timing data.
|
|
196
|
+
- **Single instance**: Analyzes one application instance at a time. For clustered applications, point to each instance separately.
|
|
197
|
+
- **Metric accumulation**: Some metrics (HTTP request counts, error rates) require traffic to accumulate data. A freshly started app may show zeros.
|
|
198
|
+
- **Environment masking**: Spring Boot masks sensitive properties by default. The `analyze_env` tool sees masked values (e.g., `******`) and cannot detect actual credential exposure in masked properties.
|
|
199
|
+
- **Custom health indicators**: The tool recognizes standard health indicator patterns. Custom health indicators with non-standard status values may not trigger specific recommendations.
|
|
200
|
+
- **Cache analysis**: Supports ConcurrentMapCache, Caffeine, Redis, and EhCache. Other cache providers may show limited analysis.
|
|
201
|
+
- **Non-JSON responses**: Handles HTML error pages (401, 403, 500) gracefully with "Invalid JSON" warnings, but cannot extract useful data from them.
|
|
202
|
+
- **Circular dependency depth**: Detects A→B→A cycles but may miss longer chains (A→B→C→A) in complex applications.
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spring Boot Actuator beans analyzer.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes /beans endpoint data to detect:
|
|
5
|
+
* - Circular dependencies
|
|
6
|
+
* - Scope mismatches (singleton depending on prototype)
|
|
7
|
+
* - Bean count per context
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Parse and analyze the /beans endpoint response.
|
|
11
|
+
*/
|
|
12
|
+
export function analyzeBeans(json) {
|
|
13
|
+
let data;
|
|
14
|
+
try {
|
|
15
|
+
data = JSON.parse(json);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {
|
|
19
|
+
totalBeans: 0,
|
|
20
|
+
contexts: [],
|
|
21
|
+
beans: [],
|
|
22
|
+
issues: [{ severity: "CRITICAL", message: "Invalid JSON in beans response", beans: [] }],
|
|
23
|
+
recommendations: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const beans = [];
|
|
27
|
+
const contexts = [];
|
|
28
|
+
const issues = [];
|
|
29
|
+
const recommendations = [];
|
|
30
|
+
// Spring Boot format: { contexts: { "application": { beans: { ... } } } }
|
|
31
|
+
const ctxs = data.contexts;
|
|
32
|
+
if (ctxs && typeof ctxs === "object") {
|
|
33
|
+
for (const [ctxName, ctxData] of Object.entries(ctxs)) {
|
|
34
|
+
contexts.push(ctxName);
|
|
35
|
+
const ctxBeans = ctxData.beans;
|
|
36
|
+
if (ctxBeans) {
|
|
37
|
+
extractBeans(ctxBeans, beans);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Flat format: { beans: { ... } } or just bean map directly
|
|
42
|
+
const flatBeans = data.beans;
|
|
43
|
+
if (flatBeans && typeof flatBeans === "object") {
|
|
44
|
+
extractBeans(flatBeans, beans);
|
|
45
|
+
}
|
|
46
|
+
// If we still have no beans, try treating the whole object as a bean map
|
|
47
|
+
if (beans.length === 0 && !ctxs && !flatBeans) {
|
|
48
|
+
for (const [name, val] of Object.entries(data)) {
|
|
49
|
+
if (val && typeof val === "object" && "scope" in val) {
|
|
50
|
+
const beanData = val;
|
|
51
|
+
beans.push({
|
|
52
|
+
name,
|
|
53
|
+
scope: beanData.scope ?? "singleton",
|
|
54
|
+
type: beanData.type ?? "unknown",
|
|
55
|
+
dependencies: beanData.dependencies ?? [],
|
|
56
|
+
resource: beanData.resource ?? null,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Analyze
|
|
62
|
+
detectCircularDependencies(beans, issues);
|
|
63
|
+
detectScopeMismatches(beans, issues, recommendations);
|
|
64
|
+
analyzeBeanCount(beans, issues, recommendations);
|
|
65
|
+
return {
|
|
66
|
+
totalBeans: beans.length,
|
|
67
|
+
contexts,
|
|
68
|
+
beans,
|
|
69
|
+
issues,
|
|
70
|
+
recommendations,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function extractBeans(beanMap, beans) {
|
|
74
|
+
for (const [name, beanData] of Object.entries(beanMap)) {
|
|
75
|
+
beans.push({
|
|
76
|
+
name,
|
|
77
|
+
scope: beanData.scope ?? "singleton",
|
|
78
|
+
type: beanData.type ?? "unknown",
|
|
79
|
+
dependencies: beanData.dependencies ?? [],
|
|
80
|
+
resource: beanData.resource ?? null,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Detect circular dependencies by building a dependency graph and finding cycles.
|
|
86
|
+
*/
|
|
87
|
+
function detectCircularDependencies(beans, issues) {
|
|
88
|
+
const beanNames = new Set(beans.map(b => b.name));
|
|
89
|
+
const adjList = new Map();
|
|
90
|
+
for (const bean of beans) {
|
|
91
|
+
const validDeps = bean.dependencies.filter(d => beanNames.has(d));
|
|
92
|
+
adjList.set(bean.name, validDeps);
|
|
93
|
+
}
|
|
94
|
+
const visited = new Set();
|
|
95
|
+
const inStack = new Set();
|
|
96
|
+
const cycles = [];
|
|
97
|
+
function dfs(node, path) {
|
|
98
|
+
if (inStack.has(node)) {
|
|
99
|
+
// Found a cycle
|
|
100
|
+
const cycleStart = path.indexOf(node);
|
|
101
|
+
if (cycleStart >= 0) {
|
|
102
|
+
cycles.push(path.slice(cycleStart));
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (visited.has(node))
|
|
107
|
+
return;
|
|
108
|
+
visited.add(node);
|
|
109
|
+
inStack.add(node);
|
|
110
|
+
path.push(node);
|
|
111
|
+
const deps = adjList.get(node) || [];
|
|
112
|
+
for (const dep of deps) {
|
|
113
|
+
dfs(dep, [...path]);
|
|
114
|
+
}
|
|
115
|
+
inStack.delete(node);
|
|
116
|
+
}
|
|
117
|
+
for (const bean of beans) {
|
|
118
|
+
if (!visited.has(bean.name)) {
|
|
119
|
+
dfs(bean.name, []);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Deduplicate cycles
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
for (const cycle of cycles) {
|
|
125
|
+
const key = [...cycle].sort().join(",");
|
|
126
|
+
if (seen.has(key))
|
|
127
|
+
continue;
|
|
128
|
+
seen.add(key);
|
|
129
|
+
issues.push({
|
|
130
|
+
severity: "WARNING",
|
|
131
|
+
message: `Circular dependency detected: ${cycle.join(" → ")} → ${cycle[0]}`,
|
|
132
|
+
beans: cycle,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Detect scope mismatches: singleton beans depending on prototype beans.
|
|
138
|
+
*/
|
|
139
|
+
function detectScopeMismatches(beans, issues, recommendations) {
|
|
140
|
+
const beansByName = new Map(beans.map(b => [b.name, b]));
|
|
141
|
+
for (const bean of beans) {
|
|
142
|
+
if (bean.scope !== "singleton")
|
|
143
|
+
continue;
|
|
144
|
+
for (const dep of bean.dependencies) {
|
|
145
|
+
const depBean = beansByName.get(dep);
|
|
146
|
+
if (depBean && depBean.scope === "prototype") {
|
|
147
|
+
issues.push({
|
|
148
|
+
severity: "WARNING",
|
|
149
|
+
message: `Singleton '${bean.name}' depends on prototype '${dep}'. The prototype will only be injected once — it won't create new instances per use.`,
|
|
150
|
+
beans: [bean.name, dep],
|
|
151
|
+
});
|
|
152
|
+
recommendations.push(`Inject ObjectFactory<${depBean.type}> or ObjectProvider<${depBean.type}> instead of direct injection for prototype bean '${dep}'.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Analyze overall bean count and flag potential issues.
|
|
159
|
+
*/
|
|
160
|
+
function analyzeBeanCount(beans, issues, recommendations) {
|
|
161
|
+
if (beans.length > 500) {
|
|
162
|
+
issues.push({
|
|
163
|
+
severity: "INFO",
|
|
164
|
+
message: `Application has ${beans.length} beans. Large bean count may affect startup time.`,
|
|
165
|
+
beans: [],
|
|
166
|
+
});
|
|
167
|
+
recommendations.push("Consider using @Lazy annotation on beans that aren't needed at startup, or use spring.main.lazy-initialization=true for development.");
|
|
168
|
+
}
|
|
169
|
+
// Find beans with many dependencies (potential god objects)
|
|
170
|
+
const highDep = beans.filter(b => b.dependencies.length > 10);
|
|
171
|
+
if (highDep.length > 0) {
|
|
172
|
+
issues.push({
|
|
173
|
+
severity: "INFO",
|
|
174
|
+
message: `${highDep.length} bean(s) have >10 dependencies: ${highDep.map(b => `${b.name} (${b.dependencies.length})`).join(", ")}. These may be doing too much.`,
|
|
175
|
+
beans: highDep.map(b => b.name),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=beans.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spring Boot cache analyzer.
|
|
3
|
+
*
|
|
4
|
+
* Parses the /caches actuator endpoint.
|
|
5
|
+
* Detects:
|
|
6
|
+
* - Caches with zero hits (dead configuration)
|
|
7
|
+
* - Low hit ratios (ineffective caching)
|
|
8
|
+
* - Missing cache metrics
|
|
9
|
+
*/
|
|
10
|
+
export function analyzeCaches(json) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
const recommendations = [];
|
|
13
|
+
const caches = [];
|
|
14
|
+
let data;
|
|
15
|
+
try {
|
|
16
|
+
data = JSON.parse(json);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
issues.push({
|
|
20
|
+
severity: "CRITICAL",
|
|
21
|
+
message: "Invalid JSON — could not parse caches endpoint response.",
|
|
22
|
+
cache: "",
|
|
23
|
+
});
|
|
24
|
+
return { caches, issues, recommendations };
|
|
25
|
+
}
|
|
26
|
+
const cacheManagers = data?.cacheManagers;
|
|
27
|
+
if (!cacheManagers || Object.keys(cacheManagers).length === 0) {
|
|
28
|
+
issues.push({
|
|
29
|
+
severity: "INFO",
|
|
30
|
+
message: "No cache managers found. Application may not be using Spring Cache.",
|
|
31
|
+
cache: "",
|
|
32
|
+
});
|
|
33
|
+
return { caches, issues, recommendations };
|
|
34
|
+
}
|
|
35
|
+
for (const [managerName, manager] of Object.entries(cacheManagers)) {
|
|
36
|
+
const managerCaches = manager.caches;
|
|
37
|
+
if (!managerCaches)
|
|
38
|
+
continue;
|
|
39
|
+
for (const [cacheName, cacheData] of Object.entries(managerCaches)) {
|
|
40
|
+
const target = cacheData.target || "unknown";
|
|
41
|
+
caches.push({
|
|
42
|
+
name: cacheName,
|
|
43
|
+
cacheManager: managerName,
|
|
44
|
+
target,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (caches.length === 0) {
|
|
49
|
+
issues.push({
|
|
50
|
+
severity: "INFO",
|
|
51
|
+
message: "No caches registered. Consider adding @Cacheable to frequently accessed data.",
|
|
52
|
+
cache: "",
|
|
53
|
+
});
|
|
54
|
+
recommendations.push("Add @Cacheable annotations to methods that read static or slowly-changing data.");
|
|
55
|
+
return { caches, issues, recommendations };
|
|
56
|
+
}
|
|
57
|
+
// Analyze cache metrics if present (from /metrics endpoint data embedded)
|
|
58
|
+
// The /caches endpoint itself just lists caches; real metrics need /metrics/cache.gets etc.
|
|
59
|
+
// We analyze what we can from the structure.
|
|
60
|
+
// Check for many caches (may indicate over-caching)
|
|
61
|
+
if (caches.length > 20) {
|
|
62
|
+
issues.push({
|
|
63
|
+
severity: "WARNING",
|
|
64
|
+
message: `${caches.length} caches registered — consider consolidating to reduce memory overhead.`,
|
|
65
|
+
cache: "",
|
|
66
|
+
});
|
|
67
|
+
recommendations.push("Review cache configurations. Each cache consumes memory. Consolidate similar caches.");
|
|
68
|
+
}
|
|
69
|
+
// Check for caches using simple (unbounded) provider
|
|
70
|
+
for (const cache of caches) {
|
|
71
|
+
if (cache.target.includes("ConcurrentMapCache") || cache.target.includes("simple")) {
|
|
72
|
+
issues.push({
|
|
73
|
+
severity: "WARNING",
|
|
74
|
+
message: `Cache "${cache.name}" uses unbounded ConcurrentMapCache — no eviction policy, will grow indefinitely.`,
|
|
75
|
+
cache: cache.name,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (issues.filter(i => i.severity !== "INFO").length === 0) {
|
|
80
|
+
recommendations.push(`${caches.length} cache(s) configured. For deeper analysis, check /metrics/cache.gets and /metrics/cache.puts for hit/miss ratios.`);
|
|
81
|
+
}
|
|
82
|
+
return { caches, issues, recommendations };
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=caches.js.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spring Boot Actuator environment risk analyzer.
|
|
3
|
+
*
|
|
4
|
+
* Analyzes /env endpoint data to detect:
|
|
5
|
+
* - Exposed secrets and credentials
|
|
6
|
+
* - Risky configurations
|
|
7
|
+
* - Missing production-critical settings
|
|
8
|
+
*/
|
|
9
|
+
// Patterns that indicate credentials or secrets
|
|
10
|
+
const SECRET_PATTERNS = [
|
|
11
|
+
/password/i, /secret/i, /api[._-]?key/i, /token/i,
|
|
12
|
+
/credential/i, /private[._-]?key/i, /access[._-]?key/i,
|
|
13
|
+
/auth/i, /jwt/i,
|
|
14
|
+
];
|
|
15
|
+
// Properties that should differ between dev and production
|
|
16
|
+
const PRODUCTION_CHECKS = [
|
|
17
|
+
{
|
|
18
|
+
property: "spring.jpa.hibernate.ddl-auto",
|
|
19
|
+
badValues: ["create", "create-drop", "update"],
|
|
20
|
+
message: "Hibernate ddl-auto is set to a destructive mode. In production, use 'validate' or 'none'.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
property: "spring.jpa.show-sql",
|
|
24
|
+
badValues: ["true"],
|
|
25
|
+
message: "SQL logging is enabled. This hurts performance and may expose sensitive data in production.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
property: "spring.h2.console.enabled",
|
|
29
|
+
badValues: ["true"],
|
|
30
|
+
message: "H2 console is enabled. This is a security risk in production — disable it.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
property: "debug",
|
|
34
|
+
badValues: ["true"],
|
|
35
|
+
message: "Debug mode is enabled. This exposes verbose logging and may leak sensitive information.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
property: "management.endpoints.web.exposure.include",
|
|
39
|
+
badValues: ["*"],
|
|
40
|
+
message: "All actuator endpoints are exposed. In production, expose only health and metrics.",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
property: "server.error.include-stacktrace",
|
|
44
|
+
badValues: ["always", "on_param"],
|
|
45
|
+
message: "Stack traces are included in error responses. This leaks internal details to clients.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
property: "spring.devtools.restart.enabled",
|
|
49
|
+
badValues: ["true"],
|
|
50
|
+
message: "DevTools restart is enabled. This should not be active in production.",
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Analyze the /env endpoint response for security and configuration risks.
|
|
55
|
+
*/
|
|
56
|
+
export function analyzeEnv(json) {
|
|
57
|
+
let data;
|
|
58
|
+
try {
|
|
59
|
+
data = JSON.parse(json);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return {
|
|
63
|
+
activeProfiles: [],
|
|
64
|
+
propertySources: [],
|
|
65
|
+
risks: [{ severity: "CRITICAL", property: "parser", message: "Invalid JSON in env response", recommendation: "Check the /env endpoint format." }],
|
|
66
|
+
recommendations: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const activeProfiles = data.activeProfiles ?? [];
|
|
70
|
+
const propertySources = [];
|
|
71
|
+
const risks = [];
|
|
72
|
+
const recommendations = [];
|
|
73
|
+
// Extract property sources
|
|
74
|
+
const sources = data.propertySources;
|
|
75
|
+
if (Array.isArray(sources)) {
|
|
76
|
+
for (const source of sources) {
|
|
77
|
+
const name = source.name;
|
|
78
|
+
if (name)
|
|
79
|
+
propertySources.push(name);
|
|
80
|
+
const properties = source.properties;
|
|
81
|
+
if (properties && typeof properties === "object") {
|
|
82
|
+
analyzeProperties(properties, risks);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Flat properties format (simpler JSON structure)
|
|
87
|
+
if (!sources && typeof data === "object") {
|
|
88
|
+
const flatProps = {};
|
|
89
|
+
for (const [key, val] of Object.entries(data)) {
|
|
90
|
+
if (key !== "activeProfiles" && key !== "propertySources") {
|
|
91
|
+
flatProps[key] = { value: val };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (Object.keys(flatProps).length > 0) {
|
|
95
|
+
analyzeProperties(flatProps, risks);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Profile-based recommendations
|
|
99
|
+
if (activeProfiles.length === 0) {
|
|
100
|
+
risks.push({
|
|
101
|
+
severity: "WARNING",
|
|
102
|
+
property: "spring.profiles.active",
|
|
103
|
+
message: "No active profiles set. The application is running with default configuration.",
|
|
104
|
+
recommendation: "Set spring.profiles.active to 'production' or 'prod' for production deployments.",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const hasProductionProfile = activeProfiles.some(p => ["production", "prod", "live"].includes(p.toLowerCase()));
|
|
108
|
+
if (!hasProductionProfile && activeProfiles.length > 0) {
|
|
109
|
+
risks.push({
|
|
110
|
+
severity: "INFO",
|
|
111
|
+
property: "spring.profiles.active",
|
|
112
|
+
message: `Active profiles: [${activeProfiles.join(", ")}]. No production profile detected.`,
|
|
113
|
+
recommendation: "Verify this is not a production environment. If it is, add a 'production' profile.",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// General recommendations
|
|
117
|
+
if (risks.filter(r => r.severity === "CRITICAL").length > 0) {
|
|
118
|
+
recommendations.push("Address all CRITICAL issues before deploying to production.");
|
|
119
|
+
}
|
|
120
|
+
if (propertySources.some(s => s.includes("application-dev") || s.includes("application-local"))) {
|
|
121
|
+
recommendations.push("Development property sources detected. Ensure production deployments use production-specific configuration.");
|
|
122
|
+
}
|
|
123
|
+
return { activeProfiles, propertySources, risks, recommendations };
|
|
124
|
+
}
|
|
125
|
+
function analyzeProperties(properties, risks) {
|
|
126
|
+
for (const [propName, propData] of Object.entries(properties)) {
|
|
127
|
+
const value = String(propData.value ?? "");
|
|
128
|
+
// Check for exposed secrets
|
|
129
|
+
checkForSecrets(propName, value, risks);
|
|
130
|
+
// Check for risky production configurations
|
|
131
|
+
checkProductionRisks(propName, value, risks);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function checkForSecrets(propName, value, risks) {
|
|
135
|
+
// Skip if value is already masked (Spring Boot masks sensitive values with *****)
|
|
136
|
+
if (value.includes("******") || value === "******")
|
|
137
|
+
return;
|
|
138
|
+
const isSecretProperty = SECRET_PATTERNS.some(pattern => pattern.test(propName));
|
|
139
|
+
if (!isSecretProperty)
|
|
140
|
+
return;
|
|
141
|
+
// Check if the value looks like a real secret (not a placeholder or empty)
|
|
142
|
+
if (value.length > 5 && value !== "null" && value !== "undefined" && !value.startsWith("${")) {
|
|
143
|
+
risks.push({
|
|
144
|
+
severity: "CRITICAL",
|
|
145
|
+
property: propName,
|
|
146
|
+
message: `Property '${propName}' appears to contain an unmasked secret value.`,
|
|
147
|
+
recommendation: `Configure management.endpoint.env.keys-to-sanitize to mask '${propName}', or use a vault/secrets manager.`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function checkProductionRisks(propName, value, risks) {
|
|
152
|
+
for (const check of PRODUCTION_CHECKS) {
|
|
153
|
+
if (propName === check.property || propName.endsWith("." + check.property)) {
|
|
154
|
+
if (check.badValues.includes(value.toLowerCase())) {
|
|
155
|
+
risks.push({
|
|
156
|
+
severity: "WARNING",
|
|
157
|
+
property: propName,
|
|
158
|
+
message: check.message,
|
|
159
|
+
recommendation: `Change '${propName}' for production environments.`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=env-risk.js.map
|