secmanifest 1.0.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/README.md +308 -0
- package/package.json +54 -0
- package/src/cli.ts +138 -0
- package/src/commands/audit.ts +249 -0
- package/src/commands/fix.ts +88 -0
- package/src/commands/watch.ts +40 -0
- package/src/core/fixer.ts +89 -0
- package/src/core/html-report.ts +259 -0
- package/src/core/notify.ts +153 -0
- package/src/core/package-manager.ts +85 -0
- package/src/core/project-analyzer.ts +84 -0
- package/src/core/reporter.ts +170 -0
- package/src/i18n/index.ts +256 -0
- package/src/scanners/backdoors.ts +192 -0
- package/src/scanners/binaries.ts +102 -0
- package/src/scanners/bundle-size.ts +114 -0
- package/src/scanners/duplicates.ts +116 -0
- package/src/scanners/integrity.ts +108 -0
- package/src/scanners/licenses.ts +111 -0
- package/src/scanners/lockfile-drift.ts +182 -0
- package/src/scanners/malware.ts +148 -0
- package/src/scanners/metadata.ts +148 -0
- package/src/scanners/node-version.ts +71 -0
- package/src/scanners/obfuscation.ts +151 -0
- package/src/scanners/outdated.ts +76 -0
- package/src/scanners/secrets.ts +224 -0
- package/src/scanners/socket-dev.ts +140 -0
- package/src/scanners/transitive.ts +97 -0
- package/src/scanners/vulnerabilities.ts +63 -0
- package/src/utils/cache.ts +59 -0
- package/src/utils/http.ts +134 -0
- package/src/utils/registry.ts +170 -0
- package/src/utils/types.ts +67 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { AuditReport, Finding, Severity } from "../utils/types.js";
|
|
4
|
+
|
|
5
|
+
const SEVERITY_COLORS: Record<Severity, string> = {
|
|
6
|
+
critical: "#FF0000",
|
|
7
|
+
high: "#FF6600",
|
|
8
|
+
medium: "#FFCC00",
|
|
9
|
+
low: "#00CCFF",
|
|
10
|
+
info: "#888888",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const CATEGORY_LABELS: Record<string, string> = {
|
|
14
|
+
vulnerability: "Vulnerabilidad",
|
|
15
|
+
malware: "Malware",
|
|
16
|
+
backdoor: "Backdoor",
|
|
17
|
+
drift: "Drift",
|
|
18
|
+
secrets: "Secrets",
|
|
19
|
+
license: "Licencia",
|
|
20
|
+
metadata: "Metadata",
|
|
21
|
+
obfuscation: "Ofuscacion",
|
|
22
|
+
"node-version": "Node.js",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function escapeHtml(str: string): string {
|
|
26
|
+
return str
|
|
27
|
+
.replace(/&/g, "&")
|
|
28
|
+
.replace(/</g, "<")
|
|
29
|
+
.replace(/>/g, ">")
|
|
30
|
+
.replace(/"/g, """);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function generateFindingsHtml(findings: Finding[]): string {
|
|
34
|
+
if (findings.length === 0) {
|
|
35
|
+
return '<div class="no-findings">No se encontraron problemas de seguridad.</div>';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const sorted = [...findings].sort((a, b) => {
|
|
39
|
+
const order: Record<Severity, number> = {
|
|
40
|
+
critical: 0, high: 1, medium: 2, low: 3, info: 4,
|
|
41
|
+
};
|
|
42
|
+
return order[a.severity] - order[b.severity];
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return `
|
|
46
|
+
<table>
|
|
47
|
+
<thead>
|
|
48
|
+
<tr>
|
|
49
|
+
<th>Severidad</th>
|
|
50
|
+
<th>Categoria</th>
|
|
51
|
+
<th>Titulo</th>
|
|
52
|
+
<th>Paquete</th>
|
|
53
|
+
<th>Recomendacion</th>
|
|
54
|
+
</tr>
|
|
55
|
+
</thead>
|
|
56
|
+
<tbody>
|
|
57
|
+
${sorted.map((f) => `
|
|
58
|
+
<tr>
|
|
59
|
+
<td><span class="severity severity-${f.severity}">${f.severity.toUpperCase()}</span></td>
|
|
60
|
+
<td>${CATEGORY_LABELS[f.category] ?? f.category}</td>
|
|
61
|
+
<td>${escapeHtml(f.title)}</td>
|
|
62
|
+
<td>${f.package ? `<code>${escapeHtml(f.package)}</code>` : "-"}</td>
|
|
63
|
+
<td>${f.recommendation ? escapeHtml(f.recommendation) : "-"}</td>
|
|
64
|
+
</tr>
|
|
65
|
+
`).join("")}
|
|
66
|
+
</tbody>
|
|
67
|
+
</table>
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function generateScoreBarHtml(score: number): string {
|
|
72
|
+
const color =
|
|
73
|
+
score >= 80 ? "#FF0000" :
|
|
74
|
+
score >= 60 ? "#FF6600" :
|
|
75
|
+
score >= 40 ? "#FFCC00" :
|
|
76
|
+
score >= 20 ? "#00CCFF" : "#00CC00";
|
|
77
|
+
|
|
78
|
+
return `
|
|
79
|
+
<div class="score-container">
|
|
80
|
+
<div class="score-bar">
|
|
81
|
+
<div class="score-fill" style="width: ${score}%; background-color: ${color};"></div>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="score-value" style="color: ${color};">${score}/100</div>
|
|
84
|
+
</div>
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function generateHtmlReport(report: AuditReport): string {
|
|
89
|
+
const criticalCount = report.findings.filter((f) => f.severity === "critical").length;
|
|
90
|
+
const highCount = report.findings.filter((f) => f.severity === "high").length;
|
|
91
|
+
const mediumCount = report.findings.filter((f) => f.severity === "medium").length;
|
|
92
|
+
const lowCount = report.findings.filter((f) => f.severity === "low").length;
|
|
93
|
+
|
|
94
|
+
return `<!DOCTYPE html>
|
|
95
|
+
<html lang="es">
|
|
96
|
+
<head>
|
|
97
|
+
<meta charset="UTF-8">
|
|
98
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
99
|
+
<title>SecManifest Audit: ${escapeHtml(report.projectName)}</title>
|
|
100
|
+
<style>
|
|
101
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
102
|
+
body {
|
|
103
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
104
|
+
background: #0d1117;
|
|
105
|
+
color: #c9d1d9;
|
|
106
|
+
padding: 2rem;
|
|
107
|
+
}
|
|
108
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
109
|
+
.header {
|
|
110
|
+
text-align: center;
|
|
111
|
+
margin-bottom: 2rem;
|
|
112
|
+
padding: 2rem;
|
|
113
|
+
background: #161b22;
|
|
114
|
+
border-radius: 12px;
|
|
115
|
+
border: 1px solid #30363d;
|
|
116
|
+
}
|
|
117
|
+
.header h1 { font-size: 2rem; color: #58a6ff; margin-bottom: 0.5rem; }
|
|
118
|
+
.header .subtitle { color: #8b949e; }
|
|
119
|
+
.stats {
|
|
120
|
+
display: grid;
|
|
121
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
122
|
+
gap: 1rem;
|
|
123
|
+
margin-bottom: 2rem;
|
|
124
|
+
}
|
|
125
|
+
.stat-card {
|
|
126
|
+
background: #161b22;
|
|
127
|
+
padding: 1.5rem;
|
|
128
|
+
border-radius: 8px;
|
|
129
|
+
border: 1px solid #30363d;
|
|
130
|
+
text-align: center;
|
|
131
|
+
}
|
|
132
|
+
.stat-card .value { font-size: 2rem; font-weight: bold; }
|
|
133
|
+
.stat-card .label { color: #8b949e; margin-top: 0.5rem; }
|
|
134
|
+
.score-container {
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
gap: 1rem;
|
|
138
|
+
margin-bottom: 2rem;
|
|
139
|
+
}
|
|
140
|
+
.score-bar {
|
|
141
|
+
flex: 1;
|
|
142
|
+
height: 24px;
|
|
143
|
+
background: #21262d;
|
|
144
|
+
border-radius: 12px;
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
}
|
|
147
|
+
.score-fill {
|
|
148
|
+
height: 100%;
|
|
149
|
+
border-radius: 12px;
|
|
150
|
+
transition: width 0.3s;
|
|
151
|
+
}
|
|
152
|
+
.score-value {
|
|
153
|
+
font-size: 1.5rem;
|
|
154
|
+
font-weight: bold;
|
|
155
|
+
min-width: 80px;
|
|
156
|
+
text-align: right;
|
|
157
|
+
}
|
|
158
|
+
table {
|
|
159
|
+
width: 100%;
|
|
160
|
+
border-collapse: collapse;
|
|
161
|
+
background: #161b22;
|
|
162
|
+
border-radius: 8px;
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
}
|
|
165
|
+
th, td {
|
|
166
|
+
padding: 0.75rem 1rem;
|
|
167
|
+
text-align: left;
|
|
168
|
+
border-bottom: 1px solid #30363d;
|
|
169
|
+
}
|
|
170
|
+
th { background: #21262d; font-weight: 600; }
|
|
171
|
+
tr:hover { background: #1c2128; }
|
|
172
|
+
.severity {
|
|
173
|
+
padding: 0.25rem 0.5rem;
|
|
174
|
+
border-radius: 4px;
|
|
175
|
+
font-size: 0.75rem;
|
|
176
|
+
font-weight: bold;
|
|
177
|
+
color: #000;
|
|
178
|
+
}
|
|
179
|
+
.severity-critical { background: #FF0000; color: #fff; }
|
|
180
|
+
.severity-high { background: #FF6600; color: #000; }
|
|
181
|
+
.severity-medium { background: #FFCC00; color: #000; }
|
|
182
|
+
.severity-low { background: #00CCFF; color: #000; }
|
|
183
|
+
.severity-info { background: #888888; color: #fff; }
|
|
184
|
+
code {
|
|
185
|
+
background: #21262d;
|
|
186
|
+
padding: 0.2rem 0.4rem;
|
|
187
|
+
border-radius: 4px;
|
|
188
|
+
font-size: 0.875rem;
|
|
189
|
+
}
|
|
190
|
+
.no-findings {
|
|
191
|
+
text-align: center;
|
|
192
|
+
padding: 3rem;
|
|
193
|
+
color: #00CC00;
|
|
194
|
+
font-size: 1.25rem;
|
|
195
|
+
}
|
|
196
|
+
.footer {
|
|
197
|
+
text-align: center;
|
|
198
|
+
margin-top: 2rem;
|
|
199
|
+
color: #8b949e;
|
|
200
|
+
font-size: 0.875rem;
|
|
201
|
+
}
|
|
202
|
+
</style>
|
|
203
|
+
</head>
|
|
204
|
+
<body>
|
|
205
|
+
<div class="container">
|
|
206
|
+
<div class="header">
|
|
207
|
+
<h1>SecManifest Security Audit</h1>
|
|
208
|
+
<div class="subtitle">${escapeHtml(report.projectName)} | ${new Date(report.timestamp).toLocaleString()}</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div class="stats">
|
|
212
|
+
<div class="stat-card">
|
|
213
|
+
<div class="value" style="color: #58a6ff;">${report.totalPackages}</div>
|
|
214
|
+
<div class="label">Paquetes</div>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="stat-card">
|
|
217
|
+
<div class="value" style="color: ${criticalCount > 0 ? '#FF0000' : '#00CC00'};">${criticalCount}</div>
|
|
218
|
+
<div class="label">Criticos</div>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="stat-card">
|
|
221
|
+
<div class="value" style="color: ${highCount > 0 ? '#FF6600' : '#00CC00'};">${highCount}</div>
|
|
222
|
+
<div class="label">Altos</div>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="stat-card">
|
|
225
|
+
<div class="value" style="color: ${mediumCount > 0 ? '#FFCC00' : '#00CC00'};">${mediumCount}</div>
|
|
226
|
+
<div class="label">Medios</div>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="stat-card">
|
|
229
|
+
<div class="value" style="color: ${lowCount > 0 ? '#00CCFF' : '#00CC00'};">${lowCount}</div>
|
|
230
|
+
<div class="label">Bajos</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<h2 style="margin-bottom: 1rem;">Score de Riesgo</h2>
|
|
235
|
+
${generateScoreBarHtml(report.score)}
|
|
236
|
+
|
|
237
|
+
<h2 style="margin-bottom: 1rem;">Hallazgos (${report.findings.length})</h2>
|
|
238
|
+
${generateFindingsHtml(report.findings)}
|
|
239
|
+
|
|
240
|
+
<div class="footer">
|
|
241
|
+
Generado por SecManifest v1.0.0 | ${report.packageManager.name ?? "desconocido"} | ${report.duration}ms
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</body>
|
|
245
|
+
</html>`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function saveHtmlReport(
|
|
249
|
+
report: AuditReport,
|
|
250
|
+
outputPath?: string,
|
|
251
|
+
): Promise<string> {
|
|
252
|
+
const html = generateHtmlReport(report);
|
|
253
|
+
const filePath = outputPath ?? join(
|
|
254
|
+
process.cwd(),
|
|
255
|
+
`secmanifest-report-${report.projectName}-${Date.now()}.html`,
|
|
256
|
+
);
|
|
257
|
+
await writeFile(filePath, html, "utf-8");
|
|
258
|
+
return filePath;
|
|
259
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import type { AuditReport } from "../utils/types.js";
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = join(homedir(), ".secmanifest");
|
|
7
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
8
|
+
|
|
9
|
+
interface SecManifestConfig {
|
|
10
|
+
slackWebhook?: string;
|
|
11
|
+
discordWebhook?: string;
|
|
12
|
+
autoFix?: boolean;
|
|
13
|
+
cacheResults?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function loadConfig(): Promise<SecManifestConfig> {
|
|
17
|
+
try {
|
|
18
|
+
const content = await readFile(CONFIG_FILE, "utf-8");
|
|
19
|
+
return JSON.parse(content);
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function saveConfig(config: SecManifestConfig): Promise<void> {
|
|
26
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
27
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function sendSlackNotification(report: AuditReport): Promise<boolean> {
|
|
31
|
+
const config = await loadConfig();
|
|
32
|
+
const webhookUrl = config.slackWebhook;
|
|
33
|
+
|
|
34
|
+
if (!webhookUrl) return false;
|
|
35
|
+
|
|
36
|
+
const criticalCount = report.findings.filter((f) => f.severity === "critical").length;
|
|
37
|
+
const highCount = report.findings.filter((f) => f.severity === "high").length;
|
|
38
|
+
|
|
39
|
+
const color =
|
|
40
|
+
report.score >= 80 ? "#FF0000" :
|
|
41
|
+
report.score >= 60 ? "#FF6600" :
|
|
42
|
+
report.score >= 40 ? "#FFCC00" : "#00CC00";
|
|
43
|
+
|
|
44
|
+
const blocks = [
|
|
45
|
+
{
|
|
46
|
+
type: "header",
|
|
47
|
+
text: {
|
|
48
|
+
type: "plain_text",
|
|
49
|
+
text: `SecManifest Audit: ${report.projectName}`,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "section",
|
|
54
|
+
fields: [
|
|
55
|
+
{ type: "mrkdwn", text: `*Score:* ${report.score}/100` },
|
|
56
|
+
{ type: "mrkdwn", text: `*Paquetes:* ${report.totalPackages}` },
|
|
57
|
+
{ type: "mrkdwn", text: `*Criticos:* ${criticalCount}` },
|
|
58
|
+
{ type: "mrkdwn", text: `*Altos:* ${highCount}` },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
if (report.findings.length > 0) {
|
|
64
|
+
const findingText = report.findings
|
|
65
|
+
.slice(0, 5)
|
|
66
|
+
.map((f) => `• [${f.severity}] ${f.title}`)
|
|
67
|
+
.join("\n");
|
|
68
|
+
|
|
69
|
+
blocks.push({
|
|
70
|
+
type: "section",
|
|
71
|
+
text: {
|
|
72
|
+
type: "mrkdwn",
|
|
73
|
+
text: `*Hallazgos:*\n${findingText}`,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(webhookUrl, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json" },
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
attachments: [
|
|
84
|
+
{
|
|
85
|
+
color,
|
|
86
|
+
blocks,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
return response.ok;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function sendDiscordNotification(report: AuditReport): Promise<boolean> {
|
|
98
|
+
const config = await loadConfig();
|
|
99
|
+
const webhookUrl = config.discordWebhook;
|
|
100
|
+
|
|
101
|
+
if (!webhookUrl) return false;
|
|
102
|
+
|
|
103
|
+
const criticalCount = report.findings.filter((f) => f.severity === "critical").length;
|
|
104
|
+
const highCount = report.findings.filter((f) => f.severity === "high").length;
|
|
105
|
+
|
|
106
|
+
const color =
|
|
107
|
+
report.score >= 80 ? 0xFF0000 :
|
|
108
|
+
report.score >= 60 ? 0xFF6600 :
|
|
109
|
+
report.score >= 40 ? 0xFFCC00 : 0x00CC00;
|
|
110
|
+
|
|
111
|
+
const embed = {
|
|
112
|
+
title: `SecManifest: ${report.projectName}`,
|
|
113
|
+
description: report.findings.length === 0
|
|
114
|
+
? "No se encontraron problemas de seguridad."
|
|
115
|
+
: report.findings
|
|
116
|
+
.slice(0, 10)
|
|
117
|
+
.map((f) => `• [${f.severity}] ${f.title}`)
|
|
118
|
+
.join("\n"),
|
|
119
|
+
color,
|
|
120
|
+
fields: [
|
|
121
|
+
{ name: "Score", value: `${report.score}/100`, inline: true },
|
|
122
|
+
{ name: "Paquetes", value: String(report.totalPackages), inline: true },
|
|
123
|
+
{ name: "Criticos", value: String(criticalCount), inline: true },
|
|
124
|
+
{ name: "Altos", value: String(highCount), inline: true },
|
|
125
|
+
],
|
|
126
|
+
footer: {
|
|
127
|
+
text: `SecManifest v1.0.0 | ${new Date().toISOString()}`,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetch(webhookUrl, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { "Content-Type": "application/json" },
|
|
135
|
+
body: JSON.stringify({ embeds: [embed] }),
|
|
136
|
+
});
|
|
137
|
+
return response.ok;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function sendNotifications(report: AuditReport): Promise<void> {
|
|
144
|
+
const config = await loadConfig();
|
|
145
|
+
|
|
146
|
+
if (config.slackWebhook) {
|
|
147
|
+
await sendSlackNotification(report);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (config.discordWebhook) {
|
|
151
|
+
await sendDiscordNotification(report);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
import type { PackageManager } from "../utils/types.js";
|
|
3
|
+
|
|
4
|
+
const BLOCKED_MANAGERS = ["npm"];
|
|
5
|
+
|
|
6
|
+
const MANAGER_PRIORITY: Array<{
|
|
7
|
+
name: PackageManager["name"];
|
|
8
|
+
command: string;
|
|
9
|
+
}> = [
|
|
10
|
+
{ name: "pnpm", command: "pnpm" },
|
|
11
|
+
{ name: "bun", command: "bun" },
|
|
12
|
+
{ name: "yarn", command: "yarn" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
async function hasCommand(command: string): Promise<boolean> {
|
|
16
|
+
try {
|
|
17
|
+
if (process.platform === "win32") {
|
|
18
|
+
await $`where ${command}`.quiet();
|
|
19
|
+
} else {
|
|
20
|
+
await $`which ${command}`.quiet();
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function getVersion(command: string): Promise<string | null> {
|
|
29
|
+
try {
|
|
30
|
+
const result = await $`${command} --version`.quiet();
|
|
31
|
+
return result.stdout.toString().trim();
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function detectPackageManager(): Promise<PackageManager> {
|
|
38
|
+
const detectedNpm = await hasCommand("npm");
|
|
39
|
+
if (detectedNpm) {
|
|
40
|
+
const npmVersion = await getVersion("npm");
|
|
41
|
+
return {
|
|
42
|
+
name: "npm",
|
|
43
|
+
command: "npm",
|
|
44
|
+
version: npmVersion ?? undefined,
|
|
45
|
+
blocked: true,
|
|
46
|
+
reason:
|
|
47
|
+
"npm está bloqueado por razones de seguridad. Se requiere pnpm, bun o yarn.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const manager of MANAGER_PRIORITY) {
|
|
52
|
+
const exists = await hasCommand(manager.command);
|
|
53
|
+
if (exists) {
|
|
54
|
+
const version = await getVersion(manager.command);
|
|
55
|
+
return {
|
|
56
|
+
name: manager.name,
|
|
57
|
+
command: manager.command,
|
|
58
|
+
version: version ?? undefined,
|
|
59
|
+
blocked: false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
name: null,
|
|
66
|
+
command: "",
|
|
67
|
+
blocked: true,
|
|
68
|
+
reason: "No se encontró ningún gestor de paquetes seguro (pnpm, bun, yarn).",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getLockfileName(
|
|
73
|
+
managerName: PackageManager["name"],
|
|
74
|
+
): string | null {
|
|
75
|
+
switch (managerName) {
|
|
76
|
+
case "pnpm":
|
|
77
|
+
return "pnpm-lock.yaml";
|
|
78
|
+
case "bun":
|
|
79
|
+
return "bun.lock";
|
|
80
|
+
case "yarn":
|
|
81
|
+
return "yarn.lock";
|
|
82
|
+
default:
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
ProjectInfo,
|
|
5
|
+
PackageManager,
|
|
6
|
+
} from "../utils/types.js";
|
|
7
|
+
import { getLockfileName } from "./package-manager.js";
|
|
8
|
+
|
|
9
|
+
export async function analyzeProject(
|
|
10
|
+
projectPath: string,
|
|
11
|
+
packageManager: PackageManager,
|
|
12
|
+
): Promise<ProjectInfo> {
|
|
13
|
+
const pkgJsonPath = join(projectPath, "package.json");
|
|
14
|
+
let pkgJsonContent: string;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
pkgJsonContent = await readFile(pkgJsonPath, "utf-8");
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`No se encontró package.json en: ${projectPath}`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let pkgJson: Record<string, unknown>;
|
|
25
|
+
try {
|
|
26
|
+
pkgJson = JSON.parse(pkgJsonContent);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Error al parsear package.json en: ${projectPath}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const dependencies =
|
|
34
|
+
(pkgJson.dependencies as Record<string, string>) ?? {};
|
|
35
|
+
const devDependencies =
|
|
36
|
+
(pkgJson.devDependencies as Record<string, string>) ?? {};
|
|
37
|
+
const scripts = (pkgJson.scripts as Record<string, string>) ?? {};
|
|
38
|
+
|
|
39
|
+
const lockfileName = getLockfileName(packageManager.name);
|
|
40
|
+
let lockfilePath: string | null = null;
|
|
41
|
+
let lockfileType: "pnpm" | "bun" | "yarn" | null = null;
|
|
42
|
+
|
|
43
|
+
if (lockfileName) {
|
|
44
|
+
const potentialPath = join(projectPath, lockfileName);
|
|
45
|
+
try {
|
|
46
|
+
await stat(potentialPath);
|
|
47
|
+
lockfilePath = potentialPath;
|
|
48
|
+
lockfileType = packageManager.name as "pnpm" | "bun" | "yarn";
|
|
49
|
+
} catch {
|
|
50
|
+
// Lockfile doesn't exist
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: (pkgJson.name as string) ?? "unknown",
|
|
56
|
+
version: (pkgJson.version as string) ?? "0.0.0",
|
|
57
|
+
dependencies,
|
|
58
|
+
devDependencies,
|
|
59
|
+
scripts,
|
|
60
|
+
lockfilePath,
|
|
61
|
+
lockfileType,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function getInstalledPackages(
|
|
66
|
+
projectPath: string,
|
|
67
|
+
packageManager: PackageManager,
|
|
68
|
+
): Promise<Array<{ name: string; version: string }>> {
|
|
69
|
+
const pkgJsonPath = join(projectPath, "package.json");
|
|
70
|
+
const pkgJsonContent = await readFile(pkgJsonPath, "utf-8");
|
|
71
|
+
const pkgJson = JSON.parse(pkgJsonContent);
|
|
72
|
+
|
|
73
|
+
const allDeps: Record<string, string> = {
|
|
74
|
+
...(pkgJson.dependencies ?? {}),
|
|
75
|
+
...(pkgJson.devDependencies ?? {}),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return Object.entries(allDeps)
|
|
79
|
+
.filter(([_, version]) => typeof version === "string")
|
|
80
|
+
.map(([name, version]) => ({
|
|
81
|
+
name,
|
|
82
|
+
version: version.replace(/[\^~>=<]*/, ""),
|
|
83
|
+
}));
|
|
84
|
+
}
|