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.
@@ -0,0 +1,102 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { Finding, ScanResult, ProjectInfo } from "../utils/types.js";
4
+
5
+ const SUSPICIOUS_BINARY_PATTERNS = [
6
+ {
7
+ name: "Postinstall script",
8
+ regex: /(?:postinstall|preinstall|install)\s*[:"]/gi,
9
+ severity: "high" as const,
10
+ description: "El paquete ejecuta comandos durante la instalacion.",
11
+ },
12
+ {
13
+ name: "Native addon",
14
+ regex: /\.node['"]/g,
15
+ severity: "medium" as const,
16
+ description: "El paquete carga un binario nativo (.node).",
17
+ },
18
+ {
19
+ name: "Binary execution",
20
+ regex: /(?:exec|spawn|execSync|execFile)\s*\(\s*['"][^'"]*\.(?:exe|bin|dll|so|dylib)/gi,
21
+ severity: "critical" as const,
22
+ description: "El paquete ejecuta un binario externo.",
23
+ },
24
+ ];
25
+
26
+ export async function scanBinaries(
27
+ projectInfo: ProjectInfo,
28
+ projectPath: string,
29
+ ): Promise<ScanResult> {
30
+ const startTime = Date.now();
31
+ const findings: Finding[] = [];
32
+ const errors: Error[] = [];
33
+
34
+ const allDeps = {
35
+ ...projectInfo.dependencies,
36
+ ...projectInfo.devDependencies,
37
+ };
38
+
39
+ for (const [name] of Object.entries(allDeps)) {
40
+ const pkgDir = join(projectPath, "node_modules", name);
41
+
42
+ try {
43
+ const pkgJsonPath = join(pkgDir, "package.json");
44
+ const content = await readFile(pkgJsonPath, "utf-8");
45
+ const pkgJson = JSON.parse(content);
46
+
47
+ if (pkgJson.bin) {
48
+ const binaries = typeof pkgJson.bin === "string"
49
+ ? { [name]: pkgJson.bin }
50
+ : pkgJson.bin as Record<string, string>;
51
+
52
+ for (const [binName, binPath] of Object.entries(binaries)) {
53
+ const fullPath = join(pkgDir, binPath as string);
54
+ try {
55
+ await readFile(fullPath);
56
+ } catch {
57
+ findings.push({
58
+ id: `binary-missing-${name}-${binName}`,
59
+ severity: "medium",
60
+ category: "malware",
61
+ title: `Binario referenciado no encontrado: ${name}/${binName}`,
62
+ description: `${name} referencia el binario ${binName} -> ${binPath} pero el archivo no existe.`,
63
+ package: name,
64
+ recommendation: `Reinstalar ${name} o verificar que el binario es correcto.`,
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ if (pkgJson.gypfile) {
71
+ const gypPath = join(pkgDir, "binding.gyp");
72
+ try {
73
+ const gypContent = await readFile(gypPath, "utf-8");
74
+
75
+ for (const pattern of SUSPICIOUS_BINARY_PATTERNS) {
76
+ if (pattern.regex.test(gypContent)) {
77
+ findings.push({
78
+ id: `binary-suspicious-${name}-${pattern.name}`,
79
+ severity: pattern.severity,
80
+ category: "malware",
81
+ title: `${pattern.name} en codigo nativo: ${name}`,
82
+ description: `${name}: ${pattern.description}`,
83
+ package: name,
84
+ recommendation: `Verificar que el codigo nativo de ${name} es seguro.`,
85
+ });
86
+ }
87
+ }
88
+ } catch {
89
+ // binding.gyp not found
90
+ }
91
+ }
92
+ } catch {
93
+ // Package directory not accessible
94
+ }
95
+ }
96
+
97
+ return {
98
+ findings,
99
+ errors,
100
+ duration: Date.now() - startTime,
101
+ };
102
+ }
@@ -0,0 +1,114 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { Finding, ScanResult, ProjectInfo } from "../utils/types.js";
4
+
5
+ const LARGE_PACKAGE_THRESHOLD = 50 * 1024 * 1024;
6
+ const MEDIUM_PACKAGE_THRESHOLD = 10 * 1024 * 1024;
7
+
8
+ async function getDirSize(dirPath: string): Promise<number> {
9
+ let totalSize = 0;
10
+
11
+ try {
12
+ const entries = await readdir(dirPath, { withFileTypes: true });
13
+
14
+ for (const entry of entries) {
15
+ const fullPath = join(dirPath, entry.name);
16
+ if (entry.isDirectory()) {
17
+ totalSize += await getDirSize(fullPath);
18
+ } else {
19
+ try {
20
+ const fileStat = await stat(fullPath);
21
+ totalSize += fileStat.size;
22
+ } catch {
23
+ // Skip
24
+ }
25
+ }
26
+ }
27
+ } catch {
28
+ // Directory not accessible
29
+ }
30
+
31
+ return totalSize;
32
+ }
33
+
34
+ function formatSize(bytes: number): string {
35
+ if (bytes >= 1024 * 1024 * 1024) {
36
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
37
+ }
38
+ if (bytes >= 1024 * 1024) {
39
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
40
+ }
41
+ if (bytes >= 1024) {
42
+ return `${(bytes / 1024).toFixed(2)} KB`;
43
+ }
44
+ return `${bytes} B`;
45
+ }
46
+
47
+ export async function scanBundleSize(
48
+ projectInfo: ProjectInfo,
49
+ projectPath: string,
50
+ ): Promise<ScanResult> {
51
+ const startTime = Date.now();
52
+ const findings: Finding[] = [];
53
+ const errors: Error[] = [];
54
+
55
+ const allDeps = {
56
+ ...projectInfo.dependencies,
57
+ ...projectInfo.devDependencies,
58
+ };
59
+
60
+ const packageSizes: Array<{ name: string; size: number }> = [];
61
+
62
+ for (const [name] of Object.entries(allDeps)) {
63
+ const pkgDir = join(projectPath, "node_modules", name);
64
+ try {
65
+ const size = await getDirSize(pkgDir);
66
+ packageSizes.push({ name, size });
67
+ } catch {
68
+ // Package not accessible
69
+ }
70
+ }
71
+
72
+ const totalSize = packageSizes.reduce((sum, p) => sum + p.size, 0);
73
+
74
+ if (totalSize > 500 * 1024 * 1024) {
75
+ findings.push({
76
+ id: "bundle-total-large",
77
+ severity: "medium",
78
+ category: "vulnerability",
79
+ title: `node_modules muy grande: ${formatSize(totalSize)}`,
80
+ description: `El directorio node_modules ocupa ${formatSize(totalSize)}. Un proyecto muy grande puede indicar dependencias innecesarias.`,
81
+ recommendation: "Revisar y eliminar dependencias innecesarias.",
82
+ });
83
+ }
84
+
85
+ for (const pkg of packageSizes) {
86
+ if (pkg.size > LARGE_PACKAGE_THRESHOLD) {
87
+ findings.push({
88
+ id: `bundle-large-${pkg.name}`,
89
+ severity: "medium",
90
+ category: "malware",
91
+ title: `Paquete sospechosamente grande: ${pkg.name} (${formatSize(pkg.size)})`,
92
+ description: `${pkg.name} ocupa ${formatSize(pkg.size)}. Paquetes maliciosos suelen incluir archivos grandes para ocultar codigo.`,
93
+ package: pkg.name,
94
+ recommendation: `Revisar el contenido de ${pkg.name} para verificar que es legitimo.`,
95
+ });
96
+ } else if (pkg.size > MEDIUM_PACKAGE_THRESHOLD) {
97
+ findings.push({
98
+ id: `bundle-medium-${pkg.name}`,
99
+ severity: "low",
100
+ category: "malware",
101
+ title: `Paquete grande: ${pkg.name} (${formatSize(pkg.size)})`,
102
+ description: `${pkg.name} ocupa ${formatSize(pkg.size)}.`,
103
+ package: pkg.name,
104
+ recommendation: `Verificar que ${pkg.name} es necesario y no contiene archivos innecesarios.`,
105
+ });
106
+ }
107
+ }
108
+
109
+ return {
110
+ findings,
111
+ errors,
112
+ duration: Date.now() - startTime,
113
+ };
114
+ }
@@ -0,0 +1,116 @@
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 DuplicateInfo {
6
+ name: string;
7
+ versions: Map<string, string[]>;
8
+ }
9
+
10
+ export async function scanDuplicates(
11
+ projectInfo: ProjectInfo,
12
+ projectPath: string,
13
+ ): Promise<ScanResult> {
14
+ const startTime = Date.now();
15
+ const findings: Finding[] = [];
16
+ const errors: Error[] = [];
17
+
18
+ const allDeps = {
19
+ ...projectInfo.dependencies,
20
+ ...projectInfo.devDependencies,
21
+ };
22
+
23
+ const versionMap = new Map<string, string[]>();
24
+
25
+ for (const [name, version] of Object.entries(allDeps)) {
26
+ const versions = versionMap.get(name) ?? [];
27
+ versions.push(version);
28
+ versionMap.set(name, versions);
29
+ }
30
+
31
+ for (const pkgDir of ["node_modules"]) {
32
+ try {
33
+ const entries = await import("node:fs/promises").then((fs) =>
34
+ fs.readdir(join(projectPath, pkgDir), { withFileTypes: true })
35
+ );
36
+
37
+ for (const entry of entries) {
38
+ if (entry.name.startsWith(".")) continue;
39
+
40
+ if (entry.name.startsWith("@")) {
41
+ const scopePath = join(projectPath, pkgDir, entry.name);
42
+ const scopedEntries = await import("node:fs/promises").then((fs) =>
43
+ fs.readdir(scopePath, { withFileTypes: true })
44
+ );
45
+
46
+ for (const scopedEntry of scopedEntries) {
47
+ if (scopedEntry.isDirectory()) {
48
+ const fullName = `${entry.name}/${scopedEntry.name}`;
49
+ const pkgJsonPath = join(scopePath, scopedEntry.name, "package.json");
50
+ try {
51
+ const content = await readFile(pkgJsonPath, "utf-8");
52
+ const pkgJson = JSON.parse(content);
53
+ const installedVersion = pkgJson.version as string;
54
+
55
+ if (installedVersion && pkgJson.dependencies) {
56
+ for (const [depName, depVersion] of Object.entries(pkgJson.dependencies as Record<string, string>)) {
57
+ if (!versionMap.has(depName)) {
58
+ const versions = versionMap.get(depName) ?? [];
59
+ versions.push(depVersion);
60
+ versionMap.set(depName, versions);
61
+ }
62
+ }
63
+ }
64
+ } catch {
65
+ // Skip
66
+ }
67
+ }
68
+ }
69
+ } else {
70
+ const pkgJsonPath = join(projectPath, pkgDir, entry.name, "package.json");
71
+ try {
72
+ const content = await readFile(pkgJsonPath, "utf-8");
73
+ const pkgJson = JSON.parse(content);
74
+ const installedVersion = pkgJson.version as string;
75
+
76
+ if (installedVersion && pkgJson.dependencies) {
77
+ for (const [depName, depVersion] of Object.entries(pkgJson.dependencies as Record<string, string>)) {
78
+ if (!versionMap.has(depName)) {
79
+ const versions = versionMap.get(depName) ?? [];
80
+ versions.push(depVersion);
81
+ versionMap.set(depName, versions);
82
+ }
83
+ }
84
+ }
85
+ } catch {
86
+ // Skip
87
+ }
88
+ }
89
+ }
90
+ } catch {
91
+ // node_modules not accessible
92
+ }
93
+ }
94
+
95
+ for (const [name, versions] of versionMap) {
96
+ const uniqueVersions = [...new Set(versions)];
97
+ if (uniqueVersions.length > 1) {
98
+ findings.push({
99
+ id: `duplicate-${name}`,
100
+ severity: "low",
101
+ category: "drift",
102
+ title: `Dependencia duplicada: ${name}`,
103
+ description: `${name} tiene multiples versiones instaladas: ${uniqueVersions.join(", ")}. Las dependencias duplicadas incrementan el bundle size.`,
104
+ package: name,
105
+ version: uniqueVersions.join(", "),
106
+ recommendation: `Consolidar a una sola version de ${name}.`,
107
+ });
108
+ }
109
+ }
110
+
111
+ return {
112
+ findings,
113
+ errors,
114
+ duration: Date.now() - startTime,
115
+ };
116
+ }
@@ -0,0 +1,108 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import type { Finding, ScanResult, ProjectInfo } from "../utils/types.js";
5
+
6
+ async function getFileHash(filePath: string): Promise<string | null> {
7
+ try {
8
+ const content = await readFile(filePath);
9
+ return createHash("sha256").update(content).digest("hex");
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ export async function scanIntegrity(
16
+ projectInfo: ProjectInfo,
17
+ projectPath: string,
18
+ ): Promise<ScanResult> {
19
+ const startTime = Date.now();
20
+ const findings: Finding[] = [];
21
+ const errors: Error[] = [];
22
+
23
+ const allDeps = {
24
+ ...projectInfo.dependencies,
25
+ ...projectInfo.devDependencies,
26
+ };
27
+
28
+ for (const [name] of Object.entries(allDeps)) {
29
+ const pkgJsonPath = join(projectPath, "node_modules", name, "package.json");
30
+ try {
31
+ const content = await readFile(pkgJsonPath, "utf-8");
32
+ const pkgJson = JSON.parse(content);
33
+
34
+ if (pkgJson.dist?.integrity) {
35
+ const actualHash = await getFileHash(
36
+ join(projectPath, "node_modules", name, "package.json"),
37
+ );
38
+
39
+ if (actualHash && !pkgJson.dist.integrity.includes(actualHash.substring(0, 12))) {
40
+ findings.push({
41
+ id: `integrity-mismatch-${name}`,
42
+ severity: "high",
43
+ category: "malware",
44
+ title: `Integridad comprometida: ${name}`,
45
+ description: `${name} tiene un hash de integridad que no coincide. El paquete pudo haber sido modificado.`,
46
+ package: name,
47
+ recommendation: `Reinstalar ${name} con "secmanifest fix ${name}" o manualmente.`,
48
+ });
49
+ }
50
+ }
51
+
52
+ if (pkgJson.dist?.tarball) {
53
+ findings.push({
54
+ id: `integrity-tarball-${name}`,
55
+ severity: "info",
56
+ category: "vulnerability",
57
+ title: `Paquete con tarball: ${name}`,
58
+ description: `${name} se distribuye via tarball: ${pkgJson.dist.tarball}`,
59
+ package: name,
60
+ recommendation: "Verificar que el tarball proviene de una fuente confiable.",
61
+ });
62
+ }
63
+ } catch {
64
+ // Package not accessible
65
+ }
66
+ }
67
+
68
+ const lockfileIntegrity = await checkLockfileIntegrity(projectPath, projectInfo);
69
+ findings.push(...lockfileIntegrity);
70
+
71
+ return {
72
+ findings,
73
+ errors,
74
+ duration: Date.now() - startTime,
75
+ };
76
+ }
77
+
78
+ async function checkLockfileIntegrity(
79
+ projectPath: string,
80
+ projectInfo: ProjectInfo,
81
+ ): Promise<Finding[]> {
82
+ const findings: Finding[] = [];
83
+
84
+ if (!projectInfo.lockfilePath) return findings;
85
+
86
+ const lockfileContent = await readFile(projectInfo.lockfilePath, "utf-8");
87
+ const lockfileHash = createHash("sha256").update(lockfileContent).digest("hex");
88
+
89
+ const hashFilePath = `${projectInfo.lockfilePath}.hash`;
90
+ try {
91
+ const savedHash = await readFile(hashFilePath, "utf-8");
92
+ if (savedHash !== lockfileHash) {
93
+ findings.push({
94
+ id: "integrity-lockfile-changed",
95
+ severity: "high",
96
+ category: "drift",
97
+ title: "Lockfile modificado desde la ultima verificacion",
98
+ description: "El lockfile ha cambiado desde la ultima verificacion de integridad.",
99
+ recommendation: "Ejecutar un fresh install o verificar los cambios.",
100
+ });
101
+ }
102
+ } catch {
103
+ const { writeFile } = await import("node:fs/promises");
104
+ await writeFile(hashFilePath, lockfileHash, "utf-8").catch(() => {});
105
+ }
106
+
107
+ return findings;
108
+ }
@@ -0,0 +1,111 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import type { Finding, ScanResult, ProjectInfo } from "../utils/types.js";
3
+
4
+ const RESTRICTED_LICENSES: Array<{
5
+ pattern: RegExp;
6
+ severity: Finding["severity"];
7
+ name: string;
8
+ reason: string;
9
+ }> = [
10
+ {
11
+ pattern: /\bAGPL[\s-]3/i,
12
+ severity: "high",
13
+ name: "AGPL-3.0",
14
+ reason: "Requiere liberar codigo fuente completo incluso en SaaS. Riesgo de divulgacion de codigo privado.",
15
+ },
16
+ {
17
+ pattern: /\bGPL[\s-]3/i,
18
+ severity: "medium",
19
+ name: "GPL-3.0",
20
+ reason: "Requiere liberar codigo fuente de trabajos derivados. Puede forzar disclosure de codigo privado.",
21
+ },
22
+ {
23
+ pattern: /\bGPL[\s-]2/i,
24
+ severity: "medium",
25
+ name: "GPL-2.0",
26
+ reason: "Copyleft fuerte. Cualquier modificacion debe ser liberada bajo la misma licencia.",
27
+ },
28
+ {
29
+ pattern: /\bSSPL\b/i,
30
+ severity: "high",
31
+ name: "SSPL",
32
+ reason: "Server Side Public License. Requiere liberar todo el stack de servicios si se usa como SaaS.",
33
+ },
34
+ {
35
+ pattern: /\bEUPL\b/i,
36
+ severity: "low",
37
+ name: "EUPL",
38
+ reason: "European Union Public License. Copyleft con implicaciones legales en la UE.",
39
+ },
40
+ {
41
+ pattern: /\bOSL[\s-]3/i,
42
+ severity: "medium",
43
+ name: "OSL-3.0",
44
+ reason: "Open Software License. Copyleft fuerte que aplica a trabajos derivados.",
45
+ },
46
+ {
47
+ pattern: /\bCC[\s-]BY[\s-]NC/i,
48
+ severity: "low",
49
+ name: "CC-BY-NC",
50
+ reason: "Creative Commons No Comercial. Prohibe uso comercial.",
51
+ },
52
+ {
53
+ pattern: /\bCC[\s-]BY[\s-]ND/i,
54
+ severity: "low",
55
+ name: "CC-BY-ND",
56
+ reason: "Creative Commons Sin Derivadas. Prohibe modificaciones.",
57
+ },
58
+ ];
59
+
60
+ const SAFE_LICENSES = new Set([
61
+ "MIT",
62
+ "ISC",
63
+ "BSD-2-Clause",
64
+ "BSD-3-Clause",
65
+ "Apache-2.0",
66
+ "0BSD",
67
+ "Unlicense",
68
+ "CC0-1.0",
69
+ "Zlib",
70
+ "BlueOak-1.0.0",
71
+ ]);
72
+
73
+ export function scanLicenses(projectInfo: ProjectInfo): ScanResult {
74
+ const startTime = Date.now();
75
+ const findings: Finding[] = [];
76
+ const errors: Error[] = [];
77
+
78
+ const allDeps = {
79
+ ...projectInfo.dependencies,
80
+ ...projectInfo.devDependencies,
81
+ };
82
+
83
+ for (const [name, version] of Object.entries(allDeps)) {
84
+ const lowerName = name.toLowerCase();
85
+ const lowerVersion = version.toLowerCase();
86
+
87
+ for (const license of RESTRICTED_LICENSES) {
88
+ if (
89
+ license.pattern.test(lowerName) ||
90
+ license.pattern.test(lowerVersion)
91
+ ) {
92
+ findings.push({
93
+ id: `license-${license.name}-${name}`,
94
+ severity: license.severity,
95
+ category: "backdoor",
96
+ title: `Licencia restrictiva: ${name} usa ${license.name}`,
97
+ description: `${name} tiene una licencia ${license.name}. ${license.reason}`,
98
+ package: name,
99
+ version,
100
+ recommendation: `Evaluar alternativas con licencias mas permisivas (MIT, ISC, Apache-2.0).`,
101
+ });
102
+ }
103
+ }
104
+ }
105
+
106
+ return {
107
+ findings,
108
+ errors,
109
+ duration: Date.now() - startTime,
110
+ };
111
+ }