@prajwolkc/stk 0.5.1 → 0.7.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/dist/commands/brain.js +36 -3
- package/dist/mcp/server.js +397 -1
- package/dist/services/brain.d.ts +32 -0
- package/dist/services/brain.js +163 -0
- 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
package/dist/services/brain.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from
|
|
|
2
2
|
import { join, resolve, basename } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
|
+
import { execSync } from "child_process";
|
|
5
6
|
import { loadConfig } from "../lib/config.js";
|
|
6
7
|
// ──────────────────────────────────────────
|
|
7
8
|
// Storage
|
|
@@ -524,3 +525,165 @@ export async function syncBrain() {
|
|
|
524
525
|
errors: [...pushResult.errors, ...pullResult.errors],
|
|
525
526
|
};
|
|
526
527
|
}
|
|
528
|
+
/** Search brain with relevance scoring — returns entries ranked by how many terms match */
|
|
529
|
+
export function smartSearch(terms, category) {
|
|
530
|
+
const store = loadBrainStore();
|
|
531
|
+
let entries = getAllEntries(store);
|
|
532
|
+
if (category) {
|
|
533
|
+
entries = entries.filter(e => e.category === category);
|
|
534
|
+
}
|
|
535
|
+
const normalizedTerms = terms.map(t => t.toLowerCase());
|
|
536
|
+
const scored = [];
|
|
537
|
+
for (const entry of entries) {
|
|
538
|
+
const searchText = `${entry.title} ${entry.content} ${entry.tags.join(" ")}`.toLowerCase();
|
|
539
|
+
const matchedTerms = [];
|
|
540
|
+
let score = 0;
|
|
541
|
+
for (const term of normalizedTerms) {
|
|
542
|
+
if (searchText.includes(term)) {
|
|
543
|
+
matchedTerms.push(term);
|
|
544
|
+
// Title match scores higher
|
|
545
|
+
if (entry.title.toLowerCase().includes(term))
|
|
546
|
+
score += 3;
|
|
547
|
+
// Tag match scores high
|
|
548
|
+
else if (entry.tags.some(t => t.toLowerCase().includes(term)))
|
|
549
|
+
score += 2;
|
|
550
|
+
// Content match
|
|
551
|
+
else
|
|
552
|
+
score += 1;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Boost gotcha/debugging entries
|
|
556
|
+
if (entry.tags.some(t => ["gotcha", "debugging", "bug", "fix", "issue"].includes(t))) {
|
|
557
|
+
score *= 1.5;
|
|
558
|
+
}
|
|
559
|
+
if (score > 0) {
|
|
560
|
+
scored.push({ entry, score, matchedTerms });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return scored.sort((a, b) => b.score - a.score);
|
|
564
|
+
}
|
|
565
|
+
/** Extract relevant terms from a task description */
|
|
566
|
+
export function extractTerms(description) {
|
|
567
|
+
const stopWords = new Set([
|
|
568
|
+
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
569
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
570
|
+
"should", "may", "might", "shall", "can", "need", "must", "to", "of",
|
|
571
|
+
"in", "for", "on", "with", "at", "by", "from", "as", "into", "about",
|
|
572
|
+
"like", "through", "after", "before", "between", "under", "above",
|
|
573
|
+
"and", "but", "or", "not", "no", "so", "if", "then", "than", "too",
|
|
574
|
+
"very", "just", "also", "how", "what", "when", "where", "why", "which",
|
|
575
|
+
"that", "this", "these", "those", "it", "its", "i", "we", "you", "they",
|
|
576
|
+
"me", "us", "my", "our", "your", "add", "implement", "create", "build",
|
|
577
|
+
"make", "want", "get", "set", "use", "new", "update", "change",
|
|
578
|
+
]);
|
|
579
|
+
const words = description.toLowerCase()
|
|
580
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
581
|
+
.split(/\s+/)
|
|
582
|
+
.filter(w => w.length > 2 && !stopWords.has(w));
|
|
583
|
+
// Also extract multi-word phrases
|
|
584
|
+
const phrases = [];
|
|
585
|
+
const desc = description.toLowerCase();
|
|
586
|
+
const commonPhrases = [
|
|
587
|
+
"email verification", "password reset", "auth state", "user select",
|
|
588
|
+
"prisma select", "react context", "protected route", "refresh token",
|
|
589
|
+
"rate limit", "soft delete", "multi-tenant", "org scoping",
|
|
590
|
+
"file upload", "webhook", "cron job", "background job",
|
|
591
|
+
"api key", "role based", "permission", "middleware",
|
|
592
|
+
];
|
|
593
|
+
for (const phrase of commonPhrases) {
|
|
594
|
+
if (desc.includes(phrase))
|
|
595
|
+
phrases.push(phrase);
|
|
596
|
+
}
|
|
597
|
+
return [...new Set([...words, ...phrases])];
|
|
598
|
+
}
|
|
599
|
+
/** Proactive check — find gotchas relevant to a task before coding */
|
|
600
|
+
export function brainCheck(taskDescription) {
|
|
601
|
+
const terms = extractTerms(taskDescription);
|
|
602
|
+
return smartSearch(terms);
|
|
603
|
+
}
|
|
604
|
+
/** Diagnose an error — find matching patterns from past issues */
|
|
605
|
+
export function brainDiagnose(error) {
|
|
606
|
+
const terms = extractTerms(error);
|
|
607
|
+
// Add error-specific terms
|
|
608
|
+
const errorTerms = error.toLowerCase()
|
|
609
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
610
|
+
.split(/\s+/)
|
|
611
|
+
.filter(w => w.length > 3);
|
|
612
|
+
const allTerms = [...new Set([...terms, ...errorTerms])];
|
|
613
|
+
return smartSearch(allTerms);
|
|
614
|
+
}
|
|
615
|
+
/** Seed brain with curated patterns (idempotent) */
|
|
616
|
+
export function seedBrain(patterns, force = false) {
|
|
617
|
+
const store = loadBrainStore();
|
|
618
|
+
if (force) {
|
|
619
|
+
// Remove existing seed entries
|
|
620
|
+
store.global = store.global.filter(e => !e.source.startsWith("seed:"));
|
|
621
|
+
}
|
|
622
|
+
const existingIds = new Set(getAllEntries(store).map(e => e.id));
|
|
623
|
+
let added = 0;
|
|
624
|
+
let skipped = 0;
|
|
625
|
+
for (const entry of patterns) {
|
|
626
|
+
if (existingIds.has(entry.id)) {
|
|
627
|
+
skipped++;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
store.global.push(entry);
|
|
631
|
+
added++;
|
|
632
|
+
}
|
|
633
|
+
if (added > 0)
|
|
634
|
+
saveBrainStore(store);
|
|
635
|
+
return { added, skipped };
|
|
636
|
+
}
|
|
637
|
+
export function reviewDiff(diff) {
|
|
638
|
+
const results = [];
|
|
639
|
+
// Split diff into per-file chunks
|
|
640
|
+
const fileChunks = diff.split(/^diff --git /m).slice(1);
|
|
641
|
+
for (const chunk of fileChunks) {
|
|
642
|
+
const pathMatch = chunk.match(/b\/(.+?)[\s\n]/);
|
|
643
|
+
if (!pathMatch)
|
|
644
|
+
continue;
|
|
645
|
+
const filePath = pathMatch[1];
|
|
646
|
+
// Count changed lines
|
|
647
|
+
const addedLines = (chunk.match(/^\+[^+]/gm) ?? []).length;
|
|
648
|
+
const removedLines = (chunk.match(/^-[^-]/gm) ?? []).length;
|
|
649
|
+
const linesChanged = addedLines + removedLines;
|
|
650
|
+
// Extract terms from filename and changed content
|
|
651
|
+
const addedContent = (chunk.match(/^\+(.+)$/gm) ?? []).map(l => l.slice(1)).join(" ");
|
|
652
|
+
const searchText = `${filePath} ${addedContent}`;
|
|
653
|
+
const terms = extractTerms(searchText);
|
|
654
|
+
// Also add file-extension-based terms
|
|
655
|
+
if (filePath.endsWith(".prisma"))
|
|
656
|
+
terms.push("prisma", "schema", "database");
|
|
657
|
+
if (filePath.includes("auth"))
|
|
658
|
+
terms.push("auth", "authentication");
|
|
659
|
+
if (filePath.includes("route"))
|
|
660
|
+
terms.push("route", "api", "endpoint");
|
|
661
|
+
if (filePath.includes("middleware"))
|
|
662
|
+
terms.push("middleware");
|
|
663
|
+
if (filePath.includes("docker") || filePath.includes("Dockerfile"))
|
|
664
|
+
terms.push("docker", "container");
|
|
665
|
+
const matches = smartSearch([...new Set(terms)]);
|
|
666
|
+
if (matches.length > 0) {
|
|
667
|
+
results.push({
|
|
668
|
+
file: filePath,
|
|
669
|
+
linesChanged,
|
|
670
|
+
warnings: matches.slice(0, 3).map(m => ({
|
|
671
|
+
title: m.entry.title,
|
|
672
|
+
content: m.entry.content.slice(0, 300),
|
|
673
|
+
relevance: m.score,
|
|
674
|
+
source: m.entry.source,
|
|
675
|
+
})),
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return results;
|
|
680
|
+
}
|
|
681
|
+
/** Get contributor name from git config */
|
|
682
|
+
export function getContributor() {
|
|
683
|
+
try {
|
|
684
|
+
return execSync("git config user.name", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
return "unknown";
|
|
688
|
+
}
|
|
689
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface MetricEntry {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
type: "deploy" | "health_check" | "error" | "response_time";
|
|
4
|
+
value: number;
|
|
5
|
+
metadata: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
interface MetricsStore {
|
|
8
|
+
version: 1;
|
|
9
|
+
entries: MetricEntry[];
|
|
10
|
+
}
|
|
11
|
+
export declare function loadMetrics(): MetricsStore;
|
|
12
|
+
export declare function saveMetrics(store: MetricsStore): void;
|
|
13
|
+
export declare function recordMetric(type: MetricEntry["type"], value: number, metadata?: Record<string, string>): void;
|
|
14
|
+
export declare function getMetrics(type?: MetricEntry["type"], days?: number, limit?: number): MetricEntry[];
|
|
15
|
+
export declare function getBaseline(type: MetricEntry["type"]): number;
|
|
16
|
+
export declare function compareToBaseline(type: MetricEntry["type"]): {
|
|
17
|
+
current: number;
|
|
18
|
+
baseline: number;
|
|
19
|
+
changePct: number;
|
|
20
|
+
status: "degraded" | "stable" | "improved";
|
|
21
|
+
};
|
|
22
|
+
export declare function getDeployFrequency(days?: number): {
|
|
23
|
+
total: number;
|
|
24
|
+
perDay: number;
|
|
25
|
+
};
|
|
26
|
+
export declare function getErrorRate(days?: number): {
|
|
27
|
+
total: number;
|
|
28
|
+
perDay: number;
|
|
29
|
+
};
|
|
30
|
+
export declare function getUptime(days?: number): {
|
|
31
|
+
checks: number;
|
|
32
|
+
healthy: number;
|
|
33
|
+
pct: number;
|
|
34
|
+
};
|
|
35
|
+
export {};
|
|
@@ -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.0",
|
|
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
|
],
|