@prajwolkc/stk 0.6.1 → 0.7.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/dist/commands/brain.js +36 -3
- package/dist/mcp/server.js +14 -1553
- package/dist/mcp/tools/brain.d.ts +2 -0
- package/dist/mcp/tools/brain.js +557 -0
- package/dist/mcp/tools/data.d.ts +2 -0
- package/dist/mcp/tools/data.js +385 -0
- package/dist/mcp/tools/github.d.ts +2 -0
- package/dist/mcp/tools/github.js +95 -0
- package/dist/mcp/tools/infra.d.ts +2 -0
- package/dist/mcp/tools/infra.js +263 -0
- package/dist/mcp/tools/ops.d.ts +2 -0
- package/dist/mcp/tools/ops.js +411 -0
- package/dist/mcp/tools/security.d.ts +2 -0
- package/dist/mcp/tools/security.js +25 -0
- package/dist/mcp/types.d.ts +2 -0
- package/dist/mcp/types.js +1 -0
- package/dist/services/brain-cloud.d.ts +13 -0
- package/dist/services/brain-cloud.js +131 -0
- package/dist/services/brain-extract.d.ts +14 -0
- package/dist/services/brain-extract.js +253 -0
- package/dist/services/brain-search.d.ts +33 -0
- package/dist/services/brain-search.js +153 -0
- package/dist/services/brain-store.d.ts +25 -0
- package/dist/services/brain-store.js +42 -0
- package/dist/services/brain.d.ts +19 -68
- package/dist/services/brain.js +18 -542
- package/dist/services/metrics.d.ts +35 -0
- package/dist/services/metrics.js +78 -0
- package/dist/services/security.d.ts +19 -0
- package/dist/services/security.js +194 -0
- package/package.json +2 -1
- package/src/data/seed-patterns.json +1802 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const STK_DIR = join(homedir(), ".stk");
|
|
5
|
+
const METRICS_PATH = join(STK_DIR, "metrics.json");
|
|
6
|
+
const MAX_ENTRIES = 1000;
|
|
7
|
+
function ensureDir() {
|
|
8
|
+
if (!existsSync(STK_DIR))
|
|
9
|
+
mkdirSync(STK_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
export function loadMetrics() {
|
|
12
|
+
ensureDir();
|
|
13
|
+
if (!existsSync(METRICS_PATH))
|
|
14
|
+
return { version: 1, entries: [] };
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(METRICS_PATH, "utf-8"));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { version: 1, entries: [] };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function saveMetrics(store) {
|
|
23
|
+
ensureDir();
|
|
24
|
+
writeFileSync(METRICS_PATH, JSON.stringify(store, null, 2));
|
|
25
|
+
}
|
|
26
|
+
export function recordMetric(type, value, metadata = {}) {
|
|
27
|
+
const store = loadMetrics();
|
|
28
|
+
store.entries.push({ timestamp: new Date().toISOString(), type, value, metadata });
|
|
29
|
+
// Evict oldest if over limit
|
|
30
|
+
if (store.entries.length > MAX_ENTRIES) {
|
|
31
|
+
store.entries = store.entries.slice(-MAX_ENTRIES);
|
|
32
|
+
}
|
|
33
|
+
saveMetrics(store);
|
|
34
|
+
}
|
|
35
|
+
export function getMetrics(type, days = 7, limit = 100) {
|
|
36
|
+
const store = loadMetrics();
|
|
37
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
38
|
+
let entries = store.entries.filter(e => e.timestamp >= since);
|
|
39
|
+
if (type)
|
|
40
|
+
entries = entries.filter(e => e.type === type);
|
|
41
|
+
return entries.slice(-limit);
|
|
42
|
+
}
|
|
43
|
+
export function getBaseline(type) {
|
|
44
|
+
const store = loadMetrics();
|
|
45
|
+
const ofType = store.entries.filter(e => e.type === type).slice(-10);
|
|
46
|
+
if (ofType.length === 0)
|
|
47
|
+
return 0;
|
|
48
|
+
return ofType.reduce((sum, e) => sum + e.value, 0) / ofType.length;
|
|
49
|
+
}
|
|
50
|
+
export function compareToBaseline(type) {
|
|
51
|
+
const store = loadMetrics();
|
|
52
|
+
const ofType = store.entries.filter(e => e.type === type);
|
|
53
|
+
if (ofType.length < 2)
|
|
54
|
+
return { current: 0, baseline: 0, changePct: 0, status: "stable" };
|
|
55
|
+
const recent = ofType.slice(-3);
|
|
56
|
+
const older = ofType.slice(-13, -3);
|
|
57
|
+
const current = recent.reduce((s, e) => s + e.value, 0) / recent.length;
|
|
58
|
+
const baseline = older.length > 0 ? older.reduce((s, e) => s + e.value, 0) / older.length : current;
|
|
59
|
+
const changePct = baseline === 0 ? 0 : Math.round(((current - baseline) / baseline) * 100);
|
|
60
|
+
const status = changePct > 20 ? "degraded" : changePct < -20 ? "improved" : "stable";
|
|
61
|
+
return { current: Math.round(current * 100) / 100, baseline: Math.round(baseline * 100) / 100, changePct, status };
|
|
62
|
+
}
|
|
63
|
+
export function getDeployFrequency(days = 7) {
|
|
64
|
+
const entries = getMetrics("deploy", days, 1000);
|
|
65
|
+
const perDay = days > 0 ? Math.round((entries.length / days) * 10) / 10 : 0;
|
|
66
|
+
return { total: entries.length, perDay };
|
|
67
|
+
}
|
|
68
|
+
export function getErrorRate(days = 7) {
|
|
69
|
+
const entries = getMetrics("error", days, 1000);
|
|
70
|
+
const perDay = days > 0 ? Math.round((entries.length / days) * 10) / 10 : 0;
|
|
71
|
+
return { total: entries.length, perDay };
|
|
72
|
+
}
|
|
73
|
+
export function getUptime(days = 7) {
|
|
74
|
+
const entries = getMetrics("health_check", days, 1000);
|
|
75
|
+
const healthy = entries.filter(e => e.value > 0).length;
|
|
76
|
+
const pct = entries.length > 0 ? Math.round((healthy / entries.length) * 1000) / 10 : 100;
|
|
77
|
+
return { checks: entries.length, healthy, pct };
|
|
78
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SecurityFinding {
|
|
2
|
+
check: string;
|
|
3
|
+
level: "critical" | "warning" | "info";
|
|
4
|
+
message: string;
|
|
5
|
+
file?: string;
|
|
6
|
+
fix?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Check for exposed secrets in .env files */
|
|
9
|
+
export declare function checkSecrets(): SecurityFinding[];
|
|
10
|
+
/** Check for outdated/vulnerable dependencies */
|
|
11
|
+
export declare function checkDeps(): SecurityFinding[];
|
|
12
|
+
/** Check for missing rate limiting */
|
|
13
|
+
export declare function checkRateLimit(): SecurityFinding[];
|
|
14
|
+
/** Check for open CORS */
|
|
15
|
+
export declare function checkCors(): SecurityFinding[];
|
|
16
|
+
/** Check for routes without auth middleware */
|
|
17
|
+
export declare function checkAuth(): SecurityFinding[];
|
|
18
|
+
/** Run all security checks */
|
|
19
|
+
export declare function runAllChecks(checks?: string[]): SecurityFinding[];
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { resolve, join } from "path";
|
|
4
|
+
/** Check for exposed secrets in .env files */
|
|
5
|
+
export function checkSecrets() {
|
|
6
|
+
const findings = [];
|
|
7
|
+
const envFiles = [".env", ".env.local", ".env.production", ".env.staging"];
|
|
8
|
+
// Check if .gitignore exists and covers env files
|
|
9
|
+
let gitignoreContent = "";
|
|
10
|
+
if (existsSync(".gitignore")) {
|
|
11
|
+
gitignoreContent = readFileSync(".gitignore", "utf-8");
|
|
12
|
+
}
|
|
13
|
+
for (const file of envFiles) {
|
|
14
|
+
if (!existsSync(file))
|
|
15
|
+
continue;
|
|
16
|
+
if (!gitignoreContent.includes(file) && !gitignoreContent.includes(".env*") && !gitignoreContent.includes(".env")) {
|
|
17
|
+
findings.push({
|
|
18
|
+
check: "secrets",
|
|
19
|
+
level: "critical",
|
|
20
|
+
message: `${file} is NOT in .gitignore — secrets may be committed`,
|
|
21
|
+
file,
|
|
22
|
+
fix: `Add "${file}" to .gitignore`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const content = readFileSync(file, "utf-8");
|
|
26
|
+
const lines = content.split("\n");
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
const [key, ...rest] = line.split("=");
|
|
29
|
+
const value = rest.join("=").trim();
|
|
30
|
+
if (!key || !value || key.startsWith("#"))
|
|
31
|
+
continue;
|
|
32
|
+
// Check for known secret patterns
|
|
33
|
+
if (value.startsWith("AKIA") && key.includes("AWS")) {
|
|
34
|
+
findings.push({ check: "secrets", level: "critical", message: `AWS access key found in ${file}`, file, fix: "Use environment variables or secrets manager" });
|
|
35
|
+
}
|
|
36
|
+
if (value.startsWith("sk_live_")) {
|
|
37
|
+
findings.push({ check: "secrets", level: "critical", message: `Live Stripe secret key in ${file}`, file, fix: "Use sk_test_ for development" });
|
|
38
|
+
}
|
|
39
|
+
if (key.includes("JWT_SECRET") && value.length < 32) {
|
|
40
|
+
findings.push({ check: "secrets", level: "warning", message: `JWT_SECRET is short (${value.length} chars) — use at least 32`, file, fix: "Generate a longer secret: openssl rand -hex 32" });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return findings;
|
|
45
|
+
}
|
|
46
|
+
/** Check for outdated/vulnerable dependencies */
|
|
47
|
+
export function checkDeps() {
|
|
48
|
+
const findings = [];
|
|
49
|
+
const pkgPaths = ["package.json", "node-backend/package.json", "frontend/package.json", "backend/package.json"];
|
|
50
|
+
for (const pkg of pkgPaths) {
|
|
51
|
+
if (!existsSync(pkg))
|
|
52
|
+
continue;
|
|
53
|
+
const dir = pkg === "package.json" ? "." : pkg.replace("/package.json", "");
|
|
54
|
+
try {
|
|
55
|
+
const result = execSync(`npm audit --json 2>/dev/null`, { cwd: resolve(dir), encoding: "utf-8", timeout: 30000 });
|
|
56
|
+
const audit = JSON.parse(result);
|
|
57
|
+
const vulns = audit.metadata?.vulnerabilities ?? {};
|
|
58
|
+
const critical = vulns.critical ?? 0;
|
|
59
|
+
const high = vulns.high ?? 0;
|
|
60
|
+
const moderate = vulns.moderate ?? 0;
|
|
61
|
+
if (critical > 0) {
|
|
62
|
+
findings.push({ check: "deps", level: "critical", message: `${critical} critical vulnerabilities in ${dir}`, file: pkg, fix: "Run: npm audit fix" });
|
|
63
|
+
}
|
|
64
|
+
if (high > 0) {
|
|
65
|
+
findings.push({ check: "deps", level: "warning", message: `${high} high vulnerabilities in ${dir}`, file: pkg, fix: "Run: npm audit fix" });
|
|
66
|
+
}
|
|
67
|
+
if (moderate > 0) {
|
|
68
|
+
findings.push({ check: "deps", level: "info", message: `${moderate} moderate vulnerabilities in ${dir}`, file: pkg });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// npm audit returns non-zero when vulns found
|
|
73
|
+
try {
|
|
74
|
+
const stderr = execSync(`npm audit --json 2>&1 || true`, { cwd: resolve(dir), encoding: "utf-8", timeout: 30000 });
|
|
75
|
+
const audit = JSON.parse(stderr);
|
|
76
|
+
const vulns = audit.metadata?.vulnerabilities ?? {};
|
|
77
|
+
if ((vulns.critical ?? 0) > 0) {
|
|
78
|
+
findings.push({ check: "deps", level: "critical", message: `${vulns.critical} critical vulnerabilities in ${dir}`, file: pkg });
|
|
79
|
+
}
|
|
80
|
+
if ((vulns.high ?? 0) > 0) {
|
|
81
|
+
findings.push({ check: "deps", level: "warning", message: `${vulns.high} high vulnerabilities in ${dir}`, file: pkg });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch { /* skip */ }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return findings;
|
|
88
|
+
}
|
|
89
|
+
/** Check for missing rate limiting */
|
|
90
|
+
export function checkRateLimit() {
|
|
91
|
+
const findings = [];
|
|
92
|
+
const routeDirs = ["src/routes", "node-backend/src/routes", "backend/src/routes", "api/src/routes"];
|
|
93
|
+
for (const dir of routeDirs) {
|
|
94
|
+
if (!existsSync(dir))
|
|
95
|
+
continue;
|
|
96
|
+
// Check if rate limiter middleware exists
|
|
97
|
+
const middlewareDirs = [
|
|
98
|
+
dir.replace("/routes", "/middleware"),
|
|
99
|
+
dir.replace("/routes", "/middlewares"),
|
|
100
|
+
];
|
|
101
|
+
let hasRateLimit = false;
|
|
102
|
+
for (const mDir of middlewareDirs) {
|
|
103
|
+
if (!existsSync(mDir))
|
|
104
|
+
continue;
|
|
105
|
+
const files = readdirSync(mDir);
|
|
106
|
+
for (const f of files) {
|
|
107
|
+
const content = readFileSync(join(mDir, f), "utf-8");
|
|
108
|
+
if (content.includes("rate") && content.includes("limit")) {
|
|
109
|
+
hasRateLimit = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!hasRateLimit) {
|
|
115
|
+
findings.push({
|
|
116
|
+
check: "rate_limit",
|
|
117
|
+
level: "warning",
|
|
118
|
+
message: `No rate limiting middleware found in ${dir.replace("/routes", "/middleware")}`,
|
|
119
|
+
fix: "Add express-rate-limit or similar middleware",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
return findings;
|
|
125
|
+
}
|
|
126
|
+
/** Check for open CORS */
|
|
127
|
+
export function checkCors() {
|
|
128
|
+
const findings = [];
|
|
129
|
+
const searchFiles = ["src/index.ts", "src/app.ts", "src/server.ts", "node-backend/src/index.ts", "node-backend/src/app.ts"];
|
|
130
|
+
for (const file of searchFiles) {
|
|
131
|
+
if (!existsSync(file))
|
|
132
|
+
continue;
|
|
133
|
+
const content = readFileSync(file, "utf-8");
|
|
134
|
+
if (content.includes("origin: '*'") || content.includes('origin: "*"')) {
|
|
135
|
+
findings.push({ check: "cors", level: "warning", message: `CORS allows all origins (*) in ${file}`, file, fix: "Restrict CORS to your frontend domain" });
|
|
136
|
+
}
|
|
137
|
+
if (content.includes("cors()") && !content.includes("origin")) {
|
|
138
|
+
findings.push({ check: "cors", level: "info", message: `CORS initialized without explicit origin in ${file}`, file, fix: "Set explicit CORS origin" });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return findings;
|
|
142
|
+
}
|
|
143
|
+
/** Check for routes without auth middleware */
|
|
144
|
+
export function checkAuth() {
|
|
145
|
+
const findings = [];
|
|
146
|
+
const routeDirs = ["src/routes", "node-backend/src/routes", "backend/src/routes"];
|
|
147
|
+
for (const dir of routeDirs) {
|
|
148
|
+
if (!existsSync(dir))
|
|
149
|
+
continue;
|
|
150
|
+
const files = readdirSync(dir).filter(f => f.endsWith(".ts") || f.endsWith(".js"));
|
|
151
|
+
const authKeywords = ["authenticate", "requirePermission", "authorize", "auth", "requireAuth", "isAuthenticated", "protect"];
|
|
152
|
+
let unprotectedCount = 0;
|
|
153
|
+
for (const file of files) {
|
|
154
|
+
if (file === "auth.ts" || file === "auth.js" || file === "health.ts")
|
|
155
|
+
continue; // auth routes are public
|
|
156
|
+
const content = readFileSync(join(dir, file), "utf-8");
|
|
157
|
+
const hasAuth = authKeywords.some(kw => content.includes(kw));
|
|
158
|
+
const hasRoutes = /router\.(get|post|put|patch|delete)\s*\(/.test(content);
|
|
159
|
+
if (hasRoutes && !hasAuth) {
|
|
160
|
+
findings.push({ check: "auth", level: "warning", message: `No auth middleware detected in ${file}`, file: join(dir, file), fix: "Add authenticate() or requirePermission() middleware" });
|
|
161
|
+
unprotectedCount++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (unprotectedCount > 0) {
|
|
165
|
+
findings.push({ check: "auth", level: "info", message: `${unprotectedCount} route files without visible auth middleware`, fix: "Review each route file for proper authentication" });
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
return findings;
|
|
170
|
+
}
|
|
171
|
+
/** Run all security checks */
|
|
172
|
+
export function runAllChecks(checks) {
|
|
173
|
+
const allChecks = {
|
|
174
|
+
secrets: checkSecrets,
|
|
175
|
+
deps: checkDeps,
|
|
176
|
+
rate_limit: checkRateLimit,
|
|
177
|
+
cors: checkCors,
|
|
178
|
+
auth: checkAuth,
|
|
179
|
+
};
|
|
180
|
+
const toRun = checks ?? Object.keys(allChecks);
|
|
181
|
+
const findings = [];
|
|
182
|
+
for (const check of toRun) {
|
|
183
|
+
const fn = allChecks[check];
|
|
184
|
+
if (fn) {
|
|
185
|
+
try {
|
|
186
|
+
findings.push(...fn());
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
findings.push({ check, level: "info", message: `Check "${check}" failed to run` });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return findings;
|
|
194
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prajwolkc/stk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "One CLI to deploy, monitor, debug, and learn about your entire stack. Infrastructure monitoring, knowledge base brain, deploy watching, and GitHub issues — all from one command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"dist",
|
|
34
|
+
"src/data",
|
|
34
35
|
"README.md",
|
|
35
36
|
"LICENSE"
|
|
36
37
|
],
|