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,76 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { $ } from "bun";
|
|
4
|
+
import type { Finding, ScanResult, ProjectInfo } from "../utils/types.js";
|
|
5
|
+
|
|
6
|
+
export async function scanOutdated(
|
|
7
|
+
projectInfo: ProjectInfo,
|
|
8
|
+
projectPath: string,
|
|
9
|
+
pmCommand: string,
|
|
10
|
+
): Promise<ScanResult> {
|
|
11
|
+
const startTime = Date.now();
|
|
12
|
+
const findings: Finding[] = [];
|
|
13
|
+
const errors: Error[] = [];
|
|
14
|
+
|
|
15
|
+
const allDeps = {
|
|
16
|
+
...projectInfo.dependencies,
|
|
17
|
+
...projectInfo.devDependencies,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (Object.keys(allDeps).length === 0) {
|
|
21
|
+
return { findings, errors, duration: Date.now() - startTime };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
let outdatedOutput: string;
|
|
26
|
+
|
|
27
|
+
if (pmCommand === "pnpm") {
|
|
28
|
+
const result = await $`${pmCommand} outdated --format json`.quiet().cwd(projectPath);
|
|
29
|
+
outdatedOutput = result.stdout.toString();
|
|
30
|
+
} else if (pmCommand === "yarn") {
|
|
31
|
+
const result = await $`${pmCommand} outdated --json`.quiet().cwd(projectPath);
|
|
32
|
+
outdatedOutput = result.stdout.toString();
|
|
33
|
+
} else {
|
|
34
|
+
const result = await $`${pmCommand} outdated --json`.quiet().cwd(projectPath);
|
|
35
|
+
outdatedOutput = result.stdout.toString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (outdatedOutput.trim()) {
|
|
39
|
+
try {
|
|
40
|
+
const outdated = JSON.parse(outdatedOutput);
|
|
41
|
+
|
|
42
|
+
const deps = outdated.dependencies ?? outdated;
|
|
43
|
+
for (const [name, info] of Object.entries(deps as Record<string, Record<string, string>>)) {
|
|
44
|
+
if (info.current && info.latest && info.current !== info.latest) {
|
|
45
|
+
const isMajor = info.latest.split(".")[0] !== info.current.split(".")[0];
|
|
46
|
+
|
|
47
|
+
findings.push({
|
|
48
|
+
id: `outdated-${name}`,
|
|
49
|
+
severity: isMajor ? "medium" : "low",
|
|
50
|
+
category: "vulnerability",
|
|
51
|
+
title: `Paquete desactualizado: ${name}`,
|
|
52
|
+
description: `${name} tiene version ${info.current} pero la ultima es ${info.latest}${isMajor ? " (cambio mayor)" : ""}.`,
|
|
53
|
+
package: name,
|
|
54
|
+
version: info.current,
|
|
55
|
+
recommendation: `Actualizar a ${info.latest}${isMajor ? " - revisar changelog por breaking changes" : ""}`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// JSON parsing failed, try line-by-line
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
errors.push(
|
|
65
|
+
new Error(
|
|
66
|
+
`Error al verificar paquetes desactualizados: ${error instanceof Error ? error.message : String(error)}`,
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
findings,
|
|
73
|
+
errors,
|
|
74
|
+
duration: Date.now() - startTime,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { Finding, ScanResult } from "../utils/types.js";
|
|
4
|
+
|
|
5
|
+
interface SecretPattern {
|
|
6
|
+
name: string;
|
|
7
|
+
regex: RegExp;
|
|
8
|
+
severity: "critical" | "high" | "medium";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SECRET_PATTERNS: SecretPattern[] = [
|
|
12
|
+
{ name: "AWS Access Key", regex: /AKIA[0-9A-Z]{16}/g, severity: "critical" },
|
|
13
|
+
{ name: "AWS Secret Key", regex: /aws[_\-]?secret[_\-]?access[_\-]?key\s*[:=]\s*['"]?[A-Za-z0-9/+=]{40}/gi, severity: "critical" },
|
|
14
|
+
{ name: "GitHub Token", regex: /gh[pousr]_[A-Za-z0-9_]{36,}/g, severity: "critical" },
|
|
15
|
+
{ name: "GitHub Fine-grained PAT", regex: /github_pat_[A-Za-z0-9_]{82,}/g, severity: "critical" },
|
|
16
|
+
{ name: "GitLab Token", regex: /glpat-[A-Za-z0-9\-_]{20,}/g, severity: "critical" },
|
|
17
|
+
{ name: "Slack Token", regex: /xox[baprs]-[0-9]{10,}-[A-Za-z0-9\-]+/g, severity: "high" },
|
|
18
|
+
{ name: "Slack Webhook", regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/g, severity: "high" },
|
|
19
|
+
{ name: "Stripe Key", regex: /[rs]k_(live|test)_[A-Za-z0-9]{24,}/g, severity: "critical" },
|
|
20
|
+
{ name: "Google API Key", regex: /AIza[A-Za-z0-9\-_]{35}/g, severity: "high" },
|
|
21
|
+
{ name: "Heroku API Key", regex: /heroku[_\-]?api[_\-]?key\s*[:=]\s*['"]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, severity: "high" },
|
|
22
|
+
{ name: "npm Token", regex: /npm_[A-Za-z0-9]{36}/g, severity: "critical" },
|
|
23
|
+
{ name: "Bearer Token", regex: /bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, severity: "high" },
|
|
24
|
+
{ name: "Private Key", regex: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/g, severity: "critical" },
|
|
25
|
+
{ name: "Connection String", regex: /(mongodb|mysql|postgres|redis):\/\/[^\s'"]+/gi, severity: "high" },
|
|
26
|
+
{ name: "Generic Secret", regex: /(secret|password|passwd|pwd)\s*[:=]\s*['"]?[^\s'"]{8,}/gi, severity: "medium" },
|
|
27
|
+
{ name: "JWT Token", regex: /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.+/=]+/g, severity: "medium" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const ENV_FILENAMES = [
|
|
31
|
+
".env",
|
|
32
|
+
".env.local",
|
|
33
|
+
".env.development",
|
|
34
|
+
".env.development.local",
|
|
35
|
+
".env.production",
|
|
36
|
+
".env.production.local",
|
|
37
|
+
".env.test",
|
|
38
|
+
".env.test.local",
|
|
39
|
+
".env.staging",
|
|
40
|
+
".env.backup",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const ALWAYS_COMMIT_PATTERNS = [
|
|
44
|
+
"AWS_ACCESS_KEY_ID",
|
|
45
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
46
|
+
"AWS_SESSION_TOKEN",
|
|
47
|
+
"DATABASE_URL",
|
|
48
|
+
"DB_PASSWORD",
|
|
49
|
+
"REDIS_URL",
|
|
50
|
+
"STRIPE_SECRET_KEY",
|
|
51
|
+
"STRIPE_PUBLISHABLE_KEY",
|
|
52
|
+
"GITHUB_TOKEN",
|
|
53
|
+
"NPM_TOKEN",
|
|
54
|
+
"SECRET_KEY",
|
|
55
|
+
"PRIVATE_KEY",
|
|
56
|
+
"API_KEY",
|
|
57
|
+
"API_SECRET",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
async function scanFileForSecrets(
|
|
61
|
+
filePath: string,
|
|
62
|
+
findings: Finding[],
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
let content: string;
|
|
65
|
+
try {
|
|
66
|
+
content = await readFile(filePath, "utf-8");
|
|
67
|
+
} catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lines = content.split("\n");
|
|
72
|
+
|
|
73
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
74
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
75
|
+
let match: RegExpExecArray | null;
|
|
76
|
+
|
|
77
|
+
while ((match = regex.exec(content)) !== null) {
|
|
78
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
79
|
+
const line = lines[lineNumber - 1]?.trim() ?? "";
|
|
80
|
+
|
|
81
|
+
if (line.startsWith("#") || line.startsWith("//")) continue;
|
|
82
|
+
if (line.startsWith("*") || line.startsWith("-")) continue;
|
|
83
|
+
if (/^\s*\{/.test(line) || /^interface\s/.test(line)) continue;
|
|
84
|
+
|
|
85
|
+
const prevLine = lines[lineNumber - 2]?.trim() ?? "";
|
|
86
|
+
if (prevLine.includes("regex:") || prevLine.includes("name:")) continue;
|
|
87
|
+
|
|
88
|
+
if (filePath.includes("scanners/secrets.ts")) {
|
|
89
|
+
if (line.includes("regex:") || line.includes("name:")) continue;
|
|
90
|
+
if (line.includes("severity:")) continue;
|
|
91
|
+
if (line.includes("SecretPattern")) continue;
|
|
92
|
+
if (line.includes("const SECRET_PATTERNS")) continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
findings.push({
|
|
96
|
+
id: `secret-${pattern.name.toLowerCase().replace(/\s+/g, "-")}-${filePath}-${lineNumber}`,
|
|
97
|
+
severity: pattern.severity,
|
|
98
|
+
category: "backdoor",
|
|
99
|
+
title: `${pattern.name} encontrado en ${filePath}:${lineNumber}`,
|
|
100
|
+
description: `Se detecto un posible ${pattern.name} en la linea ${lineNumber}. Valor parcial: ${match[0].substring(0, 20)}...`,
|
|
101
|
+
recommendation: `Mover este secreto a un archivo .env y asegurarse de que .env esta en .gitignore.`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function scanEnvFiles(
|
|
108
|
+
projectPath: string,
|
|
109
|
+
findings: Finding[],
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
for (const envFile of ENV_FILENAMES) {
|
|
112
|
+
const envPath = join(projectPath, envFile);
|
|
113
|
+
try {
|
|
114
|
+
await stat(envPath);
|
|
115
|
+
findings.push({
|
|
116
|
+
id: `secret-env-file-${envFile}`,
|
|
117
|
+
severity: "high",
|
|
118
|
+
category: "backdoor",
|
|
119
|
+
title: `Archivo de entorno expuesto: ${envFile}`,
|
|
120
|
+
description: `El archivo ${envFile} existe en el directorio del proyecto. Si esta committeado, los secretos estan expuestos.`,
|
|
121
|
+
recommendation: `Verificar que ${envFile} esta en .gitignore y no esta en el historial de git.`,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await scanFileForSecrets(envPath, findings);
|
|
125
|
+
} catch {
|
|
126
|
+
// File doesn't exist
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function scanGitignore(
|
|
132
|
+
projectPath: string,
|
|
133
|
+
findings: Finding[],
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const gitignorePath = join(projectPath, ".gitignore");
|
|
136
|
+
let content: string;
|
|
137
|
+
try {
|
|
138
|
+
content = await readFile(gitignorePath, "utf-8");
|
|
139
|
+
} catch {
|
|
140
|
+
findings.push({
|
|
141
|
+
id: "secret-no-gitignore",
|
|
142
|
+
severity: "high",
|
|
143
|
+
category: "backdoor",
|
|
144
|
+
title: "No se encontro .gitignore",
|
|
145
|
+
description: "El proyecto no tiene un archivo .gitignore. Los secretos pueden ser committeados accidentalmente.",
|
|
146
|
+
recommendation: "Crear un .gitignore que incluya .env, node_modules, y otros archivos sensibles.",
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const alwaysIgnore = [".env", ".env.*"];
|
|
152
|
+
for (const pattern of alwaysIgnore) {
|
|
153
|
+
if (!content.includes(pattern)) {
|
|
154
|
+
findings.push({
|
|
155
|
+
id: `secret-gitignore-missing-${pattern}`,
|
|
156
|
+
severity: "medium",
|
|
157
|
+
category: "backdoor",
|
|
158
|
+
title: `.gitignore no incluye ${pattern}`,
|
|
159
|
+
description: `El patron ${pattern} no esta en .gitignore. Los archivos sensibles podrian ser committeados.`,
|
|
160
|
+
recommendation: `Agregar ${pattern} a .gitignore.`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function scanSourceFiles(
|
|
167
|
+
projectPath: string,
|
|
168
|
+
findings: Finding[],
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
const sourceExtensions = [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"];
|
|
171
|
+
const ignoreDirs = ["node_modules", ".git", "dist", "build", ".next"];
|
|
172
|
+
|
|
173
|
+
async function walkDir(dir: string): Promise<void> {
|
|
174
|
+
let entries;
|
|
175
|
+
try {
|
|
176
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
177
|
+
} catch {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
if (entry.name.startsWith(".") && entry.name !== ".env") continue;
|
|
183
|
+
if (ignoreDirs.includes(entry.name)) continue;
|
|
184
|
+
|
|
185
|
+
const fullPath = join(dir, entry.name);
|
|
186
|
+
|
|
187
|
+
if (entry.isDirectory()) {
|
|
188
|
+
await walkDir(fullPath);
|
|
189
|
+
} else if (
|
|
190
|
+
sourceExtensions.some((ext) => entry.name.endsWith(ext))
|
|
191
|
+
) {
|
|
192
|
+
await scanFileForSecrets(fullPath, findings);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await walkDir(projectPath);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function scanSecrets(
|
|
201
|
+
projectPath: string,
|
|
202
|
+
): Promise<ScanResult> {
|
|
203
|
+
const startTime = Date.now();
|
|
204
|
+
const findings: Finding[] = [];
|
|
205
|
+
const errors: Error[] = [];
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await scanEnvFiles(projectPath, findings);
|
|
209
|
+
await scanGitignore(projectPath, findings);
|
|
210
|
+
await scanSourceFiles(projectPath, findings);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
errors.push(
|
|
213
|
+
new Error(
|
|
214
|
+
`Error al escanear secrets: ${error instanceof Error ? error.message : String(error)}`,
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
findings,
|
|
221
|
+
errors,
|
|
222
|
+
duration: Date.now() - startTime,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Finding, ScanResult } from "../utils/types.js";
|
|
2
|
+
import { httpRequest } from "../utils/http.js";
|
|
3
|
+
import { cacheGet, cacheSet } from "../utils/cache.js";
|
|
4
|
+
|
|
5
|
+
interface SocketPackageScore {
|
|
6
|
+
name: string;
|
|
7
|
+
score: number;
|
|
8
|
+
flags: string[];
|
|
9
|
+
details: {
|
|
10
|
+
network: number;
|
|
11
|
+
lifecycle: number;
|
|
12
|
+
codeQuality: number;
|
|
13
|
+
maintenance: number;
|
|
14
|
+
supplyChain: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SocketResponse {
|
|
19
|
+
scores: SocketPackageScore[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SOCKET_API_URL = "https://api.socket.dev/v0/packages";
|
|
23
|
+
|
|
24
|
+
const SUSPICIOUS_FLAGS = [
|
|
25
|
+
"installScripts",
|
|
26
|
+
"networkAccess",
|
|
27
|
+
"obfuscatedCode",
|
|
28
|
+
"telemetry",
|
|
29
|
+
"cryptoMining",
|
|
30
|
+
"shellExecution",
|
|
31
|
+
"fileSystemAccess",
|
|
32
|
+
"envAccess",
|
|
33
|
+
"dynamicRequire",
|
|
34
|
+
"evalUsage",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export async function scanSocketDev(
|
|
38
|
+
packages: Array<{ name: string; version: string }>,
|
|
39
|
+
): Promise<ScanResult> {
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
const findings: Finding[] = [];
|
|
42
|
+
const errors: Error[] = [];
|
|
43
|
+
|
|
44
|
+
if (packages.length === 0) {
|
|
45
|
+
return { findings, errors, duration: Date.now() - startTime };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const BATCH_SIZE = 50;
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < packages.length; i += BATCH_SIZE) {
|
|
51
|
+
const batch = packages.slice(i, i + BATCH_SIZE);
|
|
52
|
+
|
|
53
|
+
for (const pkg of batch) {
|
|
54
|
+
const cacheKey = `${pkg.name}@${pkg.version}`;
|
|
55
|
+
const cached = await cacheGet<SocketPackageScore>("socket", cacheKey);
|
|
56
|
+
if (cached) {
|
|
57
|
+
processSocketScore(cached, findings);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const response = await httpRequest<SocketPackageScore>(
|
|
63
|
+
`${SOCKET_API_URL}/${encodeURIComponent(pkg.name)}`,
|
|
64
|
+
{
|
|
65
|
+
headers: {
|
|
66
|
+
Accept: "application/json",
|
|
67
|
+
},
|
|
68
|
+
timeout: 10000,
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (response) {
|
|
73
|
+
await cacheSet("socket", cacheKey, response);
|
|
74
|
+
processSocketScore(response, findings);
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Socket.dev API not available or package not found
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
findings,
|
|
86
|
+
errors,
|
|
87
|
+
duration: Date.now() - startTime,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function processSocketScore(
|
|
92
|
+
score: SocketPackageScore,
|
|
93
|
+
findings: Finding[],
|
|
94
|
+
): void {
|
|
95
|
+
if (score.score < 50) {
|
|
96
|
+
findings.push({
|
|
97
|
+
id: `socket-low-score-${score.name}`,
|
|
98
|
+
severity: score.score < 20 ? "critical" : "high",
|
|
99
|
+
category: "malware",
|
|
100
|
+
title: `Socket.dev score bajo: ${score.name} (${score.score}/100)`,
|
|
101
|
+
description: `${score.name} tiene un score de seguridad de ${score.score}/100 en Socket.dev. Paquetes con score bajo tienen mayor riesgo de ser maliciosos.`,
|
|
102
|
+
package: score.name,
|
|
103
|
+
recommendation: `Revisar el paquete en https://socket.dev/npm/package/${score.name}`,
|
|
104
|
+
reference: `https://socket.dev/npm/package/${score.name}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const flag of score.flags) {
|
|
109
|
+
if (SUSPICIOUS_FLAGS.includes(flag)) {
|
|
110
|
+
findings.push({
|
|
111
|
+
id: `socket-flag-${flag}-${score.name}`,
|
|
112
|
+
severity: getFlagSeverity(flag),
|
|
113
|
+
category: "malware",
|
|
114
|
+
title: `Socket.dev flag: ${flag} en ${score.name}`,
|
|
115
|
+
description: `${score.name} tiene la flag "${flag}" detectada por Socket.dev.`,
|
|
116
|
+
package: score.name,
|
|
117
|
+
recommendation: `Revisar el comportamiento del paquete en Socket.dev.`,
|
|
118
|
+
reference: `https://socket.dev/npm/package/${score.name}`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getFlagSeverity(flag: string): Finding["severity"] {
|
|
125
|
+
switch (flag) {
|
|
126
|
+
case "cryptoMining":
|
|
127
|
+
case "shellExecution":
|
|
128
|
+
case "obfuscatedCode":
|
|
129
|
+
return "critical";
|
|
130
|
+
case "networkAccess":
|
|
131
|
+
case "dynamicRequire":
|
|
132
|
+
case "evalUsage":
|
|
133
|
+
return "high";
|
|
134
|
+
case "installScripts":
|
|
135
|
+
case "fileSystemAccess":
|
|
136
|
+
return "medium";
|
|
137
|
+
default:
|
|
138
|
+
return "low";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { Finding, ScanResult, ProjectInfo } from "../utils/types.js";
|
|
4
|
+
|
|
5
|
+
interface PackageJson {
|
|
6
|
+
name?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
dependencies?: Record<string, string>;
|
|
9
|
+
devDependencies?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function getAllTransitiveDeps(
|
|
13
|
+
projectPath: string,
|
|
14
|
+
deps: Record<string, string>,
|
|
15
|
+
): Promise<Map<string, string>> {
|
|
16
|
+
const allDeps = new Map<string, string>();
|
|
17
|
+
|
|
18
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
19
|
+
allDeps.set(name, version);
|
|
20
|
+
|
|
21
|
+
const pkgJsonPath = join(projectPath, "node_modules", name, "package.json");
|
|
22
|
+
try {
|
|
23
|
+
const content = await readFile(pkgJsonPath, "utf-8");
|
|
24
|
+
const pkgJson: PackageJson = JSON.parse(content);
|
|
25
|
+
|
|
26
|
+
if (pkgJson.dependencies) {
|
|
27
|
+
for (const [depName, depVersion] of Object.entries(pkgJson.dependencies)) {
|
|
28
|
+
if (!allDeps.has(depName)) {
|
|
29
|
+
allDeps.set(depName, depVersion);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Package not found
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return allDeps;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function scanTransitive(
|
|
42
|
+
projectInfo: ProjectInfo,
|
|
43
|
+
projectPath: string,
|
|
44
|
+
): Promise<ScanResult> {
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
const findings: Finding[] = [];
|
|
47
|
+
const errors: Error[] = [];
|
|
48
|
+
|
|
49
|
+
const allDeps = {
|
|
50
|
+
...projectInfo.dependencies,
|
|
51
|
+
...projectInfo.devDependencies,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const transitiveDeps = await getAllTransitiveDeps(projectPath, allDeps);
|
|
55
|
+
|
|
56
|
+
const directDepNames = new Set(Object.keys(allDeps));
|
|
57
|
+
const transitiveOnly = new Map<string, string>();
|
|
58
|
+
|
|
59
|
+
for (const [name, version] of transitiveDeps) {
|
|
60
|
+
if (!directDepNames.has(name)) {
|
|
61
|
+
transitiveOnly.set(name, version);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const [name, version] of transitiveOnly) {
|
|
66
|
+
if (version.includes("*") || version === "latest" || version === ">=0.0.0") {
|
|
67
|
+
findings.push({
|
|
68
|
+
id: `transitive-open-${name}`,
|
|
69
|
+
severity: "medium",
|
|
70
|
+
category: "vulnerability",
|
|
71
|
+
title: `Dependencia transitiva con version abierta: ${name}`,
|
|
72
|
+
description: `${name} es una dependencia transitiva con version "${version}". Las dependencias transitivas con versiones abiertas son vectores de ataque.`,
|
|
73
|
+
package: name,
|
|
74
|
+
version,
|
|
75
|
+
recommendation: `Auditar si ${name} es necesario y si tiene una version segura disponible.`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const totalTransitive = transitiveOnly.size;
|
|
81
|
+
if (totalTransitive > 50) {
|
|
82
|
+
findings.push({
|
|
83
|
+
id: "transitive-high-count",
|
|
84
|
+
severity: "low",
|
|
85
|
+
category: "vulnerability",
|
|
86
|
+
title: `Alto numero de dependencias transitivas: ${totalTransitive}`,
|
|
87
|
+
description: `El proyecto tiene ${totalTransitive} dependencias transitivas. Un arbol de dependencias grande incrementa la superficie de ataque.`,
|
|
88
|
+
recommendation: "Revisar si todas las dependencias son necesarias.",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
findings,
|
|
94
|
+
errors,
|
|
95
|
+
duration: Date.now() - startTime,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { Finding, ScanResult } from "../utils/types.js";
|
|
3
|
+
import { checkOssIndex } from "../utils/http.js";
|
|
4
|
+
|
|
5
|
+
export async function scanVulnerabilities(
|
|
6
|
+
packages: Array<{ name: string; version: string }>,
|
|
7
|
+
): Promise<ScanResult> {
|
|
8
|
+
const startTime = Date.now();
|
|
9
|
+
const findings: Finding[] = [];
|
|
10
|
+
const errors: Error[] = [];
|
|
11
|
+
|
|
12
|
+
if (packages.length === 0) {
|
|
13
|
+
return { findings, errors, duration: Date.now() - startTime };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const results = await checkOssIndex(packages);
|
|
18
|
+
|
|
19
|
+
for (const result of results) {
|
|
20
|
+
for (const vuln of result.vulnerabilities) {
|
|
21
|
+
const packageName = result.coordinates.replace(
|
|
22
|
+
"pkg:npm/",
|
|
23
|
+
"",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
let severity: Finding["severity"] = "info";
|
|
27
|
+
if (vuln.severity === "CRITICAL") severity = "critical";
|
|
28
|
+
else if (vuln.severity === "HIGH") severity = "high";
|
|
29
|
+
else if (vuln.severity === "MEDIUM") severity = "medium";
|
|
30
|
+
else if (vuln.severity === "LOW") severity = "low";
|
|
31
|
+
|
|
32
|
+
findings.push({
|
|
33
|
+
id: vuln.id,
|
|
34
|
+
severity,
|
|
35
|
+
category: "vulnerability",
|
|
36
|
+
title: vuln.title || vuln.displayName,
|
|
37
|
+
description:
|
|
38
|
+
vuln.description ||
|
|
39
|
+
`Vulnerabilidad encontrada en ${packageName}`,
|
|
40
|
+
package: packageName,
|
|
41
|
+
cve: vuln.cve || undefined,
|
|
42
|
+
reference: vuln.reference,
|
|
43
|
+
recommendation:
|
|
44
|
+
severity === "critical" || severity === "high"
|
|
45
|
+
? `Actualizar ${packageName} a la ultima version`
|
|
46
|
+
: undefined,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
errors.push(
|
|
52
|
+
new Error(
|
|
53
|
+
`Error al consultar Sonatype OSS Index: ${error instanceof Error ? error.message : String(error)}`,
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
findings,
|
|
60
|
+
errors,
|
|
61
|
+
duration: Date.now() - startTime,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
const CACHE_DIR = join(homedir(), ".secmanifest", "cache");
|
|
6
|
+
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
interface CacheEntry<T> {
|
|
9
|
+
data: T;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function ensureCacheDir(): Promise<void> {
|
|
14
|
+
try {
|
|
15
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
16
|
+
} catch {
|
|
17
|
+
// Directory exists
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getCacheKey(namespace: string, key: string): string {
|
|
22
|
+
const sanitized = key.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
23
|
+
return join(CACHE_DIR, `${namespace}_${sanitized}.json`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function cacheGet<T>(namespace: string, key: string): Promise<T | null> {
|
|
27
|
+
const cachePath = getCacheKey(namespace, key);
|
|
28
|
+
try {
|
|
29
|
+
const content = await readFile(cachePath, "utf-8");
|
|
30
|
+
const entry: CacheEntry<T> = JSON.parse(content);
|
|
31
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return entry.data;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function cacheSet<T>(namespace: string, key: string, data: T): Promise<void> {
|
|
41
|
+
await ensureCacheDir();
|
|
42
|
+
const cachePath = getCacheKey(namespace, key);
|
|
43
|
+
const entry: CacheEntry<T> = { data, timestamp: Date.now() };
|
|
44
|
+
await writeFile(cachePath, JSON.stringify(entry), "utf-8");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function cacheClear(namespace?: string): Promise<void> {
|
|
48
|
+
const { readdir, unlink } = await import("node:fs/promises");
|
|
49
|
+
try {
|
|
50
|
+
const files = await readdir(CACHE_DIR);
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
if (!namespace || file.startsWith(namespace)) {
|
|
53
|
+
await unlink(join(CACHE_DIR, file)).catch(() => {});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Cache dir doesn't exist
|
|
58
|
+
}
|
|
59
|
+
}
|