@deepv-code/safe-npm 0.1.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/LICENSE +21 -0
- package/README.md +120 -0
- package/README.zh-CN.md +121 -0
- package/dist/cli/args-parser.d.ts +11 -0
- package/dist/cli/args-parser.js +36 -0
- package/dist/cli/check.d.ts +5 -0
- package/dist/cli/check.js +126 -0
- package/dist/cli/proxy.d.ts +1 -0
- package/dist/cli/proxy.js +4 -0
- package/dist/data/popular-packages.d.ts +9 -0
- package/dist/data/popular-packages.js +83 -0
- package/dist/i18n/en.d.ts +43 -0
- package/dist/i18n/en.js +50 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.js +11 -0
- package/dist/i18n/zh.d.ts +2 -0
- package/dist/i18n/zh.js +50 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +77 -0
- package/dist/scanner/code-analyzer.d.ts +2 -0
- package/dist/scanner/code-analyzer.js +130 -0
- package/dist/scanner/index.d.ts +3 -0
- package/dist/scanner/index.js +163 -0
- package/dist/scanner/patterns/exfiltration.d.ts +7 -0
- package/dist/scanner/patterns/exfiltration.js +49 -0
- package/dist/scanner/patterns/miner.d.ts +5 -0
- package/dist/scanner/patterns/miner.js +32 -0
- package/dist/scanner/patterns/obfuscation.d.ts +15 -0
- package/dist/scanner/patterns/obfuscation.js +110 -0
- package/dist/scanner/types.d.ts +26 -0
- package/dist/scanner/types.js +1 -0
- package/dist/scanner/typosquatting.d.ts +3 -0
- package/dist/scanner/typosquatting.js +126 -0
- package/dist/scanner/virustotal.d.ts +7 -0
- package/dist/scanner/virustotal.js +249 -0
- package/dist/scanner/vulnerability.d.ts +2 -0
- package/dist/scanner/vulnerability.js +42 -0
- package/dist/tui/App.d.ts +2 -0
- package/dist/tui/App.js +67 -0
- package/dist/tui/index.d.ts +1 -0
- package/dist/tui/index.js +6 -0
- package/dist/tui/screens/CheckScreen.d.ts +7 -0
- package/dist/tui/screens/CheckScreen.js +92 -0
- package/dist/tui/screens/PopularScreen.d.ts +7 -0
- package/dist/tui/screens/PopularScreen.js +39 -0
- package/dist/tui/screens/SettingsScreen.d.ts +6 -0
- package/dist/tui/screens/SettingsScreen.js +64 -0
- package/dist/utils/config.d.ts +16 -0
- package/dist/utils/config.js +69 -0
- package/dist/utils/npm-package.d.ts +38 -0
- package/dist/utils/npm-package.js +191 -0
- package/dist/utils/npm-runner.d.ts +7 -0
- package/dist/utils/npm-runner.js +56 -0
- package/package.json +48 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { popularPackages, popularGlobalTools, popularAgents } from '../../data/popular-packages.js';
|
|
4
|
+
export function PopularScreen({ onSelect, onBack }) {
|
|
5
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
6
|
+
// Combine lists for display
|
|
7
|
+
const items = [
|
|
8
|
+
...popularAgents.map(a => ({ name: a.name, desc: a.desc })),
|
|
9
|
+
...popularGlobalTools.map(t => ({ name: t.name, desc: t.desc })),
|
|
10
|
+
...popularPackages.map(p => ({ name: p, desc: '' }))
|
|
11
|
+
];
|
|
12
|
+
const visibleItems = items.slice(0, 20); // Show only top 20 for now to avoid scrolling issues initially
|
|
13
|
+
useInput((input, key) => {
|
|
14
|
+
if (key.upArrow) {
|
|
15
|
+
setSelectedIndex(i => Math.max(0, i - 1));
|
|
16
|
+
}
|
|
17
|
+
if (key.downArrow) {
|
|
18
|
+
setSelectedIndex(i => Math.min(visibleItems.length - 1, i + 1));
|
|
19
|
+
}
|
|
20
|
+
if (key.return) {
|
|
21
|
+
onSelect(visibleItems[selectedIndex].name);
|
|
22
|
+
}
|
|
23
|
+
if (input === 'q' || key.escape) {
|
|
24
|
+
onBack();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
28
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "Popular Packages & Tools"),
|
|
29
|
+
React.createElement(Text, { dimColor: true }, "Select a package to scan it"),
|
|
30
|
+
React.createElement(Box, { flexDirection: "column", marginTop: 1 }, visibleItems.map((item, i) => (React.createElement(Box, { key: item.name },
|
|
31
|
+
React.createElement(Text, { color: i === selectedIndex ? 'green' : undefined },
|
|
32
|
+
i === selectedIndex ? '❯ ' : ' ',
|
|
33
|
+
item.name),
|
|
34
|
+
item.desc && (React.createElement(Text, { dimColor: true },
|
|
35
|
+
" - ",
|
|
36
|
+
item.desc)))))),
|
|
37
|
+
React.createElement(Box, { marginTop: 1 },
|
|
38
|
+
React.createElement(Text, { dimColor: true }, "Press Enter to scan, q to back"))));
|
|
39
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { getConfig, saveConfig } from '../../utils/config.js';
|
|
4
|
+
export function SettingsScreen({ onBack }) {
|
|
5
|
+
const [config, setConfig] = useState(getConfig());
|
|
6
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
7
|
+
const settings = [
|
|
8
|
+
{
|
|
9
|
+
key: 'language',
|
|
10
|
+
label: 'Language',
|
|
11
|
+
value: config.language,
|
|
12
|
+
toggle: () => {
|
|
13
|
+
const newLang = config.language === 'en' ? 'zh' : 'en';
|
|
14
|
+
updateConfig({ language: newLang });
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: 'vt_enabled',
|
|
19
|
+
label: 'VirusTotal Scan',
|
|
20
|
+
value: config.virustotal.enabled ? 'Enabled' : 'Disabled',
|
|
21
|
+
toggle: () => {
|
|
22
|
+
updateConfig({
|
|
23
|
+
virustotal: { ...config.virustotal, enabled: !config.virustotal.enabled }
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: 'offline',
|
|
29
|
+
label: 'Offline Mode',
|
|
30
|
+
value: config.offline ? 'On' : 'Off',
|
|
31
|
+
toggle: () => {
|
|
32
|
+
updateConfig({ offline: !config.offline });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
const updateConfig = (newPart) => {
|
|
37
|
+
saveConfig(newPart);
|
|
38
|
+
setConfig(getConfig()); // Reload to be sure
|
|
39
|
+
};
|
|
40
|
+
useInput((input, key) => {
|
|
41
|
+
if (key.upArrow) {
|
|
42
|
+
setSelectedIndex(i => Math.max(0, i - 1));
|
|
43
|
+
}
|
|
44
|
+
if (key.downArrow) {
|
|
45
|
+
setSelectedIndex(i => Math.min(settings.length - 1, i + 1));
|
|
46
|
+
}
|
|
47
|
+
if (key.return || input === ' ') {
|
|
48
|
+
settings[selectedIndex].toggle();
|
|
49
|
+
}
|
|
50
|
+
if (key.escape || input === 'q') {
|
|
51
|
+
onBack();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
55
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "Settings"),
|
|
56
|
+
React.createElement(Box, { flexDirection: "column", marginTop: 1 }, settings.map((item, i) => (React.createElement(Box, { key: item.key },
|
|
57
|
+
React.createElement(Text, { color: i === selectedIndex ? 'green' : undefined },
|
|
58
|
+
i === selectedIndex ? '❯ ' : ' ',
|
|
59
|
+
item.label,
|
|
60
|
+
": ",
|
|
61
|
+
React.createElement(Text, { bold: true }, String(item.value))))))),
|
|
62
|
+
React.createElement(Box, { marginTop: 1 },
|
|
63
|
+
React.createElement(Text, { dimColor: true }, "Space/Enter to toggle, q to back"))));
|
|
64
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Language } from '../i18n/index.js';
|
|
2
|
+
export interface Config {
|
|
3
|
+
language: Language;
|
|
4
|
+
virustotal: {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
};
|
|
8
|
+
offline: boolean;
|
|
9
|
+
cache: {
|
|
10
|
+
ttl: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare function hasConfigFile(): boolean;
|
|
14
|
+
export declare function getConfig(): Config;
|
|
15
|
+
export declare function saveConfig(config: Partial<Config>): void;
|
|
16
|
+
export declare function resetConfigCache(): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
const DEFAULT_VT_KEY = '3886ca2a3f7f9f42cb161f314cd1446fb7fd88e4097c5e004e1b64435d0726a9';
|
|
5
|
+
const defaultConfig = {
|
|
6
|
+
language: 'en',
|
|
7
|
+
virustotal: {
|
|
8
|
+
apiKey: DEFAULT_VT_KEY,
|
|
9
|
+
enabled: true,
|
|
10
|
+
},
|
|
11
|
+
offline: false,
|
|
12
|
+
cache: {
|
|
13
|
+
ttl: 86400,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
function getConfigDir() {
|
|
17
|
+
return join(homedir(), '.safe-npm');
|
|
18
|
+
}
|
|
19
|
+
function getConfigPath() {
|
|
20
|
+
return join(getConfigDir(), 'config.json');
|
|
21
|
+
}
|
|
22
|
+
export function hasConfigFile() {
|
|
23
|
+
return existsSync(getConfigPath());
|
|
24
|
+
}
|
|
25
|
+
let cachedConfig = null;
|
|
26
|
+
export function getConfig() {
|
|
27
|
+
if (cachedConfig)
|
|
28
|
+
return cachedConfig;
|
|
29
|
+
const configPath = getConfigPath();
|
|
30
|
+
if (existsSync(configPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
33
|
+
const loaded = JSON.parse(content);
|
|
34
|
+
cachedConfig = {
|
|
35
|
+
...defaultConfig,
|
|
36
|
+
...loaded,
|
|
37
|
+
virustotal: {
|
|
38
|
+
...defaultConfig.virustotal,
|
|
39
|
+
...(loaded.virustotal || {})
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
// Ensure API key is set (fallback to default if empty)
|
|
43
|
+
if (!cachedConfig?.virustotal.apiKey) {
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
cachedConfig.virustotal.apiKey = DEFAULT_VT_KEY;
|
|
46
|
+
}
|
|
47
|
+
return cachedConfig;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
cachedConfig = defaultConfig;
|
|
51
|
+
return cachedConfig;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
cachedConfig = defaultConfig;
|
|
55
|
+
return cachedConfig;
|
|
56
|
+
}
|
|
57
|
+
export function saveConfig(config) {
|
|
58
|
+
const configDir = getConfigDir();
|
|
59
|
+
const configPath = getConfigPath();
|
|
60
|
+
if (!existsSync(configDir)) {
|
|
61
|
+
mkdirSync(configDir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
const newConfig = { ...getConfig(), ...config };
|
|
64
|
+
writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
|
|
65
|
+
cachedConfig = newConfig;
|
|
66
|
+
}
|
|
67
|
+
export function resetConfigCache() {
|
|
68
|
+
cachedConfig = null;
|
|
69
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface PackageInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
version: string;
|
|
4
|
+
tarballUrl: string;
|
|
5
|
+
tarballSha: string;
|
|
6
|
+
scripts: Record<string, string>;
|
|
7
|
+
main?: string;
|
|
8
|
+
dependencies: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export interface PackageFiles {
|
|
11
|
+
packageJson: Record<string, unknown>;
|
|
12
|
+
scripts: Record<string, string>;
|
|
13
|
+
entryFile?: string;
|
|
14
|
+
entryContent?: string;
|
|
15
|
+
allJsFiles: {
|
|
16
|
+
path: string;
|
|
17
|
+
content: string;
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get package info from npm registry
|
|
22
|
+
*/
|
|
23
|
+
export declare function getPackageInfo(packageName: string): Promise<PackageInfo | null>;
|
|
24
|
+
/**
|
|
25
|
+
* Check if a package exists in the registry
|
|
26
|
+
*/
|
|
27
|
+
export declare function checkPackageExists(packageName: string): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Download package tarball and return SHA256 hash
|
|
30
|
+
*/
|
|
31
|
+
export declare function downloadPackageAndHash(tarballUrl: string): Promise<{
|
|
32
|
+
hash: string;
|
|
33
|
+
buffer: Buffer;
|
|
34
|
+
} | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Extract tarball and scan files
|
|
37
|
+
*/
|
|
38
|
+
export declare function extractAndScanPackage(packageName: string): Promise<PackageFiles | null>;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { mkdirSync, rmSync, existsSync, readFileSync } from 'fs';
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
/**
|
|
10
|
+
* Get package info from npm registry
|
|
11
|
+
*/
|
|
12
|
+
export async function getPackageInfo(packageName) {
|
|
13
|
+
try {
|
|
14
|
+
// Parse package name and version
|
|
15
|
+
let name = packageName;
|
|
16
|
+
let version = 'latest';
|
|
17
|
+
if (packageName.includes('@') && !packageName.startsWith('@')) {
|
|
18
|
+
const parts = packageName.split('@');
|
|
19
|
+
name = parts[0];
|
|
20
|
+
version = parts[1] || 'latest';
|
|
21
|
+
}
|
|
22
|
+
else if (packageName.startsWith('@')) {
|
|
23
|
+
// Scoped package like @types/node@1.0.0
|
|
24
|
+
const lastAt = packageName.lastIndexOf('@');
|
|
25
|
+
if (lastAt > 0) {
|
|
26
|
+
name = packageName.substring(0, lastAt);
|
|
27
|
+
version = packageName.substring(lastAt + 1) || 'latest';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const { stdout } = await execAsync(`npm view ${name}@${version} --json`, {
|
|
31
|
+
timeout: 30000,
|
|
32
|
+
});
|
|
33
|
+
const info = JSON.parse(stdout);
|
|
34
|
+
return {
|
|
35
|
+
name: info.name,
|
|
36
|
+
version: info.version,
|
|
37
|
+
tarballUrl: info.dist?.tarball || '',
|
|
38
|
+
tarballSha: info.dist?.shasum || '',
|
|
39
|
+
scripts: info.scripts || {},
|
|
40
|
+
main: info.main,
|
|
41
|
+
dependencies: info.dependencies || {},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.stdout && error.stdout.includes('E404')) {
|
|
46
|
+
return null; // Clearly not found
|
|
47
|
+
}
|
|
48
|
+
// For other errors, we also return null currently but log it
|
|
49
|
+
// Suppress logging if it's just a 404 (npm view output usually contains the error)
|
|
50
|
+
if (!error.message.includes('code E404')) {
|
|
51
|
+
console.error(`Failed to get package info for ${packageName}:`, error.message);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if a package exists in the registry
|
|
58
|
+
*/
|
|
59
|
+
export async function checkPackageExists(packageName) {
|
|
60
|
+
try {
|
|
61
|
+
await execAsync(`npm view ${packageName} name`, { timeout: 10000 });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Download package tarball and return SHA256 hash
|
|
70
|
+
*/
|
|
71
|
+
export async function downloadPackageAndHash(tarballUrl) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await axios.get(tarballUrl, {
|
|
74
|
+
responseType: 'arraybuffer',
|
|
75
|
+
timeout: 60000,
|
|
76
|
+
});
|
|
77
|
+
const buffer = Buffer.from(response.data);
|
|
78
|
+
const sha256 = createHash('sha256').update(buffer).digest('hex');
|
|
79
|
+
return { hash: sha256, buffer };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error('Failed to download tarball:', error);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Extract tarball and scan files
|
|
88
|
+
*/
|
|
89
|
+
export async function extractAndScanPackage(packageName) {
|
|
90
|
+
const tempDir = join(tmpdir(), `safe-npm-scan-${Date.now()}`);
|
|
91
|
+
try {
|
|
92
|
+
mkdirSync(tempDir, { recursive: true });
|
|
93
|
+
// Use npm pack to download the package
|
|
94
|
+
const { stdout } = await execAsync(`npm pack ${packageName} --pack-destination="${tempDir}"`, {
|
|
95
|
+
timeout: 60000,
|
|
96
|
+
cwd: tempDir,
|
|
97
|
+
});
|
|
98
|
+
const tgzFile = stdout.trim();
|
|
99
|
+
const tgzPath = join(tempDir, tgzFile);
|
|
100
|
+
// Extract the tarball
|
|
101
|
+
await execAsync(`tar -xzf "${tgzPath}" -C "${tempDir}"`, {
|
|
102
|
+
timeout: 30000,
|
|
103
|
+
});
|
|
104
|
+
const packageDir = join(tempDir, 'package');
|
|
105
|
+
if (!existsSync(packageDir)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// Read package.json
|
|
109
|
+
const packageJsonPath = join(packageDir, 'package.json');
|
|
110
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
111
|
+
// Collect all JS files (limit to entry point and install scripts area)
|
|
112
|
+
const allJsFiles = [];
|
|
113
|
+
const filesToScan = [];
|
|
114
|
+
// Add main entry file
|
|
115
|
+
const mainFile = packageJson.main || 'index.js';
|
|
116
|
+
const mainPath = join(packageDir, mainFile);
|
|
117
|
+
if (existsSync(mainPath)) {
|
|
118
|
+
filesToScan.push(mainPath);
|
|
119
|
+
}
|
|
120
|
+
// Add bin files
|
|
121
|
+
if (packageJson.bin) {
|
|
122
|
+
const bins = typeof packageJson.bin === 'string'
|
|
123
|
+
? [packageJson.bin]
|
|
124
|
+
: Object.values(packageJson.bin);
|
|
125
|
+
for (const bin of bins) {
|
|
126
|
+
const binPath = join(packageDir, bin);
|
|
127
|
+
if (existsSync(binPath)) {
|
|
128
|
+
filesToScan.push(binPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Scan preinstall/postinstall scripts if they reference files
|
|
133
|
+
const scripts = packageJson.scripts || {};
|
|
134
|
+
for (const [scriptName, scriptCmd] of Object.entries(scripts)) {
|
|
135
|
+
if (['preinstall', 'install', 'postinstall'].includes(scriptName)) {
|
|
136
|
+
// Check if script references a JS file
|
|
137
|
+
const cmd = scriptCmd;
|
|
138
|
+
const jsMatch = cmd.match(/node\s+([^\s]+\.js)/);
|
|
139
|
+
if (jsMatch) {
|
|
140
|
+
const scriptPath = join(packageDir, jsMatch[1]);
|
|
141
|
+
if (existsSync(scriptPath)) {
|
|
142
|
+
filesToScan.push(scriptPath);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Read files to scan
|
|
148
|
+
for (const filePath of filesToScan) {
|
|
149
|
+
try {
|
|
150
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
151
|
+
allJsFiles.push({
|
|
152
|
+
path: filePath.replace(packageDir, ''),
|
|
153
|
+
content,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Skip unreadable files
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Get entry content
|
|
161
|
+
let entryContent;
|
|
162
|
+
if (existsSync(mainPath)) {
|
|
163
|
+
try {
|
|
164
|
+
entryContent = readFileSync(mainPath, 'utf-8');
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Ignore
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
packageJson,
|
|
172
|
+
scripts: packageJson.scripts || {},
|
|
173
|
+
entryFile: mainFile,
|
|
174
|
+
entryContent,
|
|
175
|
+
allJsFiles,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error('Failed to extract package:', error);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
// Cleanup temp directory
|
|
184
|
+
try {
|
|
185
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Ignore cleanup errors
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
export function runNpm(args) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
const isWin = process.platform === 'win32';
|
|
5
|
+
const command = isWin ? 'cmd.exe' : 'npm';
|
|
6
|
+
// On Windows, use /d /s /c to run the command safely
|
|
7
|
+
// npm.cmd is usually in PATH, but we can specify it directly if needed.
|
|
8
|
+
// Usually 'npm' works inside cmd.
|
|
9
|
+
const finalArgs = isWin ? ['/d', '/s', '/c', 'npm', ...args] : args;
|
|
10
|
+
const child = spawn(command, finalArgs, {
|
|
11
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
12
|
+
shell: false,
|
|
13
|
+
});
|
|
14
|
+
let stdout = '';
|
|
15
|
+
let stderr = '';
|
|
16
|
+
child.stdout?.on('data', (data) => {
|
|
17
|
+
stdout += data.toString();
|
|
18
|
+
process.stdout.write(data);
|
|
19
|
+
});
|
|
20
|
+
child.stderr?.on('data', (data) => {
|
|
21
|
+
stderr += data.toString();
|
|
22
|
+
process.stderr.write(data);
|
|
23
|
+
});
|
|
24
|
+
child.on('close', (code) => {
|
|
25
|
+
resolve({
|
|
26
|
+
exitCode: code ?? 1,
|
|
27
|
+
stdout,
|
|
28
|
+
stderr,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
child.on('error', () => {
|
|
32
|
+
resolve({
|
|
33
|
+
exitCode: 1,
|
|
34
|
+
stdout,
|
|
35
|
+
stderr: stderr || 'Failed to run npm',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function runNpmPassthrough(args) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const isWin = process.platform === 'win32';
|
|
43
|
+
const command = isWin ? 'cmd.exe' : 'npm';
|
|
44
|
+
const finalArgs = isWin ? ['/d', '/s', '/c', 'npm', ...args] : args;
|
|
45
|
+
const child = spawn(command, finalArgs, {
|
|
46
|
+
stdio: 'inherit',
|
|
47
|
+
shell: false,
|
|
48
|
+
});
|
|
49
|
+
child.on('close', (code) => {
|
|
50
|
+
resolve(code ?? 1);
|
|
51
|
+
});
|
|
52
|
+
child.on('error', () => {
|
|
53
|
+
resolve(1);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepv-code/safe-npm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A security-focused npm wrapper that scans packages before installation",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"safe-npm": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsc -w",
|
|
14
|
+
"test": "vitest"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"npm",
|
|
18
|
+
"security",
|
|
19
|
+
"malware",
|
|
20
|
+
"scanner"
|
|
21
|
+
],
|
|
22
|
+
"author": "DeepV Code",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"README.zh-CN.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"axios": "^1.13.4",
|
|
32
|
+
"chalk": "^5.6.2",
|
|
33
|
+
"commander": "^14.0.2",
|
|
34
|
+
"form-data": "^4.0.5",
|
|
35
|
+
"ink": "^6.6.0",
|
|
36
|
+
"ink-text-input": "^6.0.0",
|
|
37
|
+
"ora": "^9.1.0",
|
|
38
|
+
"react": "^19.2.4"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/form-data": "^2.2.1",
|
|
42
|
+
"@types/ink-text-input": "^2.0.5",
|
|
43
|
+
"@types/node": "^25.0.10",
|
|
44
|
+
"@types/react": "^19.2.10",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vitest": "^4.0.18"
|
|
47
|
+
}
|
|
48
|
+
}
|