@safedep/pmg 0.0.6
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/.npmignore +17 -0
- package/README.md +60 -0
- package/bin/pmg.js +76 -0
- package/config.js +34 -0
- package/install.js +224 -0
- package/package.json +58 -0
package/.npmignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Exclude the actual binary (but keep the wrapper script)
|
|
2
|
+
bin/pmg
|
|
3
|
+
bin/pmg.exe
|
|
4
|
+
|
|
5
|
+
# Exclude temp files and directories
|
|
6
|
+
temp/
|
|
7
|
+
.temp/
|
|
8
|
+
|
|
9
|
+
# Exclude development files
|
|
10
|
+
node_modules/
|
|
11
|
+
.git/
|
|
12
|
+
.gitignore
|
|
13
|
+
*.log
|
|
14
|
+
.DS_Store
|
|
15
|
+
|
|
16
|
+
# Keep only the wrapper script in bin/
|
|
17
|
+
!bin/pmg.js
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# PMG - Package Manager Guard
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
🤖 PMG protects developers from getting compromised by malicious open source packages.
|
|
5
|
+
|
|
6
|
+
This is the npm distribution of PMG, a tool that wraps your favorite package manager (e.g., `npm`) and blocks malicious packages at install time.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Install PMG globally via npm:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g @safedep/pmg
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or using Homebrew:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
brew tap safedep/tap
|
|
20
|
+
brew install safedep/tap/pmg
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Set up PMG to automatically protect your package installations:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Recommended: Set up automatic protection
|
|
29
|
+
pmg setup install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
After setup, use your package managers normally:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Your regular commands are now protected
|
|
36
|
+
npm install express
|
|
37
|
+
pnpm add react
|
|
38
|
+
pip install requests
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or use PMG manually without setup:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Manual protection (alternative)
|
|
45
|
+
pmg npm install express
|
|
46
|
+
pmg pnpm add react
|
|
47
|
+
pmg pip install requests
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Platform Support
|
|
51
|
+
|
|
52
|
+
- ✅ **macOS** (Intel & Apple Silicon)
|
|
53
|
+
- ✅ **Linux** (x86_64, ARM64, i386)
|
|
54
|
+
- ✅ **Windows** (x86_64, ARM64, i386)
|
|
55
|
+
|
|
56
|
+
Requires Node.js 14 or higher.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
For complete documentation, advanced usage, troubleshooting, and more information, please visit: **[github.com/safedep/pmg](https://github.com/safedep/pmg)**
|
package/bin/pmg.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawn } = require("child_process");
|
|
6
|
+
const { ORG_NAME, PACKAGE_NAME, BINARY_NAME } = require("../config");
|
|
7
|
+
|
|
8
|
+
const BINARY_NAME_WITH_EXT =
|
|
9
|
+
process.platform === "win32" ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
10
|
+
const BINARY_PATH = path.join(__dirname, BINARY_NAME_WITH_EXT);
|
|
11
|
+
|
|
12
|
+
function main() {
|
|
13
|
+
// Check if binary exists
|
|
14
|
+
if (!fs.existsSync(BINARY_PATH)) {
|
|
15
|
+
console.error(`❌ ${BINARY_NAME_WITH_EXT} binary not found`);
|
|
16
|
+
console.error(
|
|
17
|
+
`Try reinstalling: npm install -g ${ORG_NAME}/${PACKAGE_NAME}`,
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Verify binary is executable
|
|
23
|
+
try {
|
|
24
|
+
fs.accessSync(BINARY_PATH, fs.constants.F_OK | fs.constants.X_OK);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`❌ ${BINARY_NAME_WITH_EXT} is not executable`);
|
|
27
|
+
console.error(
|
|
28
|
+
`Try reinstalling: npm install -g ${ORG_NAME}/${PACKAGE_NAME}`,
|
|
29
|
+
);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Pass all arguments to the binary
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
|
|
36
|
+
// Spawn the binary with inherited stdio for proper terminal interaction
|
|
37
|
+
const child = spawn(BINARY_PATH, args, {
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
windowsHide: false,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Handle process termination
|
|
43
|
+
child.on("error", (error) => {
|
|
44
|
+
console.error(
|
|
45
|
+
`❌ Failed to execute ${BINARY_NAME_WITH_EXT}: ${error.message}`,
|
|
46
|
+
);
|
|
47
|
+
console.error(
|
|
48
|
+
`Try reinstalling: npm install -g ${ORG_NAME}/${PACKAGE_NAME}`,
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Exit with the same code as the child process
|
|
54
|
+
child.on("exit", (code, signal) => {
|
|
55
|
+
if (signal) {
|
|
56
|
+
process.kill(process.pid, signal);
|
|
57
|
+
} else {
|
|
58
|
+
process.exit(code || 0);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Handle termination signals
|
|
63
|
+
process.on("SIGTERM", () => {
|
|
64
|
+
child.kill("SIGTERM");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
process.on("SIGINT", () => {
|
|
68
|
+
child.kill("SIGINT");
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (require.main === module) {
|
|
73
|
+
main();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { main };
|
package/config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Configuration for npm binary wrapper
|
|
2
|
+
|
|
3
|
+
const ORG_NAME = "@safedep";
|
|
4
|
+
const PACKAGE_NAME = "pmg";
|
|
5
|
+
const BINARY_NAME = "pmg";
|
|
6
|
+
|
|
7
|
+
// GitHub repository information for releases
|
|
8
|
+
const REPO_OWNER = "safedep";
|
|
9
|
+
const REPO_NAME = "pmg";
|
|
10
|
+
|
|
11
|
+
// GitHub releases base URL (constructed from repo info)
|
|
12
|
+
const GITHUB_RELEASES_BASE = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download`;
|
|
13
|
+
|
|
14
|
+
// Platform-specific binary filename patterns (GoReleaser format)
|
|
15
|
+
const BINARY_PATTERNS = {
|
|
16
|
+
"darwin-x64": `${BINARY_NAME}_Darwin_all.tar.gz`,
|
|
17
|
+
"darwin-arm64": `${BINARY_NAME}_Darwin_all.tar.gz`,
|
|
18
|
+
"linux-x64": `${BINARY_NAME}_Linux_x86_64.tar.gz`,
|
|
19
|
+
"linux-arm64": `${BINARY_NAME}_Linux_arm64.tar.gz`,
|
|
20
|
+
"linux-ia32": `${BINARY_NAME}_Linux_i386.tar.gz`,
|
|
21
|
+
"win32-x64": `${BINARY_NAME}_Windows_x86_64.zip`,
|
|
22
|
+
"win32-arm64": `${BINARY_NAME}_Windows_arm64.zip`,
|
|
23
|
+
"win32-ia32": `${BINARY_NAME}_Windows_i386.zip`,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
ORG_NAME,
|
|
28
|
+
PACKAGE_NAME,
|
|
29
|
+
BINARY_NAME,
|
|
30
|
+
REPO_OWNER,
|
|
31
|
+
REPO_NAME,
|
|
32
|
+
GITHUB_RELEASES_BASE,
|
|
33
|
+
BINARY_PATTERNS,
|
|
34
|
+
};
|
package/install.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const crypto = require("crypto");
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
const {
|
|
10
|
+
BINARY_NAME,
|
|
11
|
+
REPO_OWNER,
|
|
12
|
+
REPO_NAME,
|
|
13
|
+
GITHUB_RELEASES_BASE,
|
|
14
|
+
BINARY_PATTERNS,
|
|
15
|
+
} = require("./config");
|
|
16
|
+
|
|
17
|
+
// Read version from package.json with strict validation
|
|
18
|
+
function getValidatedVersion() {
|
|
19
|
+
try {
|
|
20
|
+
const packageJson = JSON.parse(
|
|
21
|
+
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const version = packageJson.version;
|
|
25
|
+
|
|
26
|
+
// Strict validation: must be valid semver (x.y.z)
|
|
27
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
28
|
+
throw new Error(`Invalid version format: ${version}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return `v${version}`;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error(`Failed to read valid version: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const RELEASE_VERSION = getValidatedVersion();
|
|
38
|
+
const BASE_URL = `${GITHUB_RELEASES_BASE}/${RELEASE_VERSION}`;
|
|
39
|
+
|
|
40
|
+
// Platform-specific binary URLs (constructed from config)
|
|
41
|
+
const BINARY_URLS = {};
|
|
42
|
+
Object.keys(BINARY_PATTERNS).forEach((platform) => {
|
|
43
|
+
BINARY_URLS[platform] = `${BASE_URL}/${BINARY_PATTERNS[platform]}`;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const CHECKSUMS_URL = `${BASE_URL}/checksums.txt`;
|
|
47
|
+
|
|
48
|
+
function getPlatformKey() {
|
|
49
|
+
const platform = process.platform;
|
|
50
|
+
const arch = process.arch;
|
|
51
|
+
return `${platform}-${arch}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function downloadFile(url, dest, maxRedirects = 5) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
if (maxRedirects < 0) {
|
|
57
|
+
reject(new Error("Too many redirects"));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const file = fs.createWriteStream(dest);
|
|
62
|
+
|
|
63
|
+
https
|
|
64
|
+
.get(url, (response) => {
|
|
65
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
66
|
+
file.close();
|
|
67
|
+
fs.unlink(dest, () => {});
|
|
68
|
+
return downloadFile(response.headers.location, dest, maxRedirects - 1)
|
|
69
|
+
.then(resolve)
|
|
70
|
+
.catch(reject);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (response.statusCode !== 200) {
|
|
74
|
+
file.close();
|
|
75
|
+
fs.unlink(dest, () => {});
|
|
76
|
+
reject(new Error(`Download failed: ${response.statusCode}`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
response.pipe(file);
|
|
81
|
+
|
|
82
|
+
file.on("finish", () => {
|
|
83
|
+
file.close();
|
|
84
|
+
resolve();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
file.on("error", (err) => {
|
|
88
|
+
fs.unlink(dest, () => {});
|
|
89
|
+
reject(err);
|
|
90
|
+
});
|
|
91
|
+
})
|
|
92
|
+
.on("error", reject);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function calculateChecksum(filePath) {
|
|
97
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
98
|
+
const hashSum = crypto.createHash("sha256");
|
|
99
|
+
hashSum.update(fileBuffer);
|
|
100
|
+
return hashSum.digest("hex");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateChecksum(filePath, expectedChecksum) {
|
|
104
|
+
const actualChecksum = calculateChecksum(filePath);
|
|
105
|
+
return actualChecksum === expectedChecksum;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractArchive(archivePath, extractDir) {
|
|
109
|
+
const isZip = archivePath.endsWith(".zip");
|
|
110
|
+
|
|
111
|
+
if (isZip) {
|
|
112
|
+
execSync(`unzip -o "${archivePath}" -d "${extractDir}"`, { stdio: "pipe" });
|
|
113
|
+
} else {
|
|
114
|
+
execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, { stdio: "pipe" });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function install() {
|
|
119
|
+
let tempWorkspace;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
console.log("📦 Installing PMG binary...");
|
|
123
|
+
|
|
124
|
+
// Get platform-specific URL
|
|
125
|
+
const platformKey = getPlatformKey();
|
|
126
|
+
const binaryUrl = BINARY_URLS[platformKey];
|
|
127
|
+
|
|
128
|
+
if (!binaryUrl) {
|
|
129
|
+
throw new Error(`Unsupported platform: ${platformKey}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`🔍 Platform: ${platformKey}`);
|
|
133
|
+
console.log(`📡 Version: ${RELEASE_VERSION}`);
|
|
134
|
+
|
|
135
|
+
// Create directories
|
|
136
|
+
const binDir = path.join(__dirname, "bin");
|
|
137
|
+
tempWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), "pmg-install-"));
|
|
138
|
+
|
|
139
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
140
|
+
|
|
141
|
+
// Download binary archive
|
|
142
|
+
const archiveFilename = path.basename(binaryUrl);
|
|
143
|
+
const archivePath = path.join(tempWorkspace, archiveFilename);
|
|
144
|
+
|
|
145
|
+
console.log(`⬇️ Downloading binary...`);
|
|
146
|
+
await downloadFile(binaryUrl, archivePath);
|
|
147
|
+
|
|
148
|
+
// Download checksums
|
|
149
|
+
const checksumsPath = path.join(tempWorkspace, "checksums.txt");
|
|
150
|
+
console.log(`⬇️ Downloading checksums...`);
|
|
151
|
+
await downloadFile(CHECKSUMS_URL, checksumsPath);
|
|
152
|
+
|
|
153
|
+
// Parse checksums file
|
|
154
|
+
const checksumsContent = fs.readFileSync(checksumsPath, "utf8");
|
|
155
|
+
const checksumLines = checksumsContent.split("\n");
|
|
156
|
+
|
|
157
|
+
let expectedChecksum = null;
|
|
158
|
+
for (const line of checksumLines) {
|
|
159
|
+
if (line.includes(archiveFilename)) {
|
|
160
|
+
expectedChecksum = line.split(/\s+/)[0];
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!expectedChecksum) {
|
|
166
|
+
throw new Error(`Checksum not found for ${archiveFilename}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate checksum
|
|
170
|
+
console.log(`🔐 Validating checksum...`);
|
|
171
|
+
if (!validateChecksum(archivePath, expectedChecksum)) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"Checksum validation failed - binary may be corrupted or tampered",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`✅ Checksum validated`);
|
|
178
|
+
|
|
179
|
+
// Extract archive
|
|
180
|
+
console.log(`📂 Extracting binary...`);
|
|
181
|
+
extractArchive(archivePath, tempWorkspace);
|
|
182
|
+
|
|
183
|
+
// Find and move binary
|
|
184
|
+
const binaryName =
|
|
185
|
+
process.platform === "win32" ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
186
|
+
const extractedBinaryPath = path.join(tempWorkspace, binaryName);
|
|
187
|
+
const finalBinaryPath = path.join(binDir, binaryName);
|
|
188
|
+
|
|
189
|
+
if (!fs.existsSync(extractedBinaryPath)) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Binary not found at expected location: ${extractedBinaryPath}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Move binary to final location
|
|
196
|
+
fs.renameSync(extractedBinaryPath, finalBinaryPath);
|
|
197
|
+
|
|
198
|
+
// Make executable on Unix systems
|
|
199
|
+
if (process.platform !== "win32") {
|
|
200
|
+
fs.chmodSync(finalBinaryPath, "755");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Clean up
|
|
204
|
+
fs.rmSync(tempWorkspace, { recursive: true, force: true });
|
|
205
|
+
|
|
206
|
+
console.log("✅ PMG binary installed successfully!");
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error("❌ Installation failed:", error.message);
|
|
209
|
+
|
|
210
|
+
// Clean up on failure
|
|
211
|
+
try {
|
|
212
|
+
if (tempWorkspace && fs.existsSync(tempWorkspace)) {
|
|
213
|
+
fs.rmSync(tempWorkspace, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
} catch (cleanupError) {
|
|
216
|
+
console.warn("⚠️ Failed to clean up:", cleanupError.message);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Run installation
|
|
224
|
+
install();
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@safedep/pmg",
|
|
3
|
+
"description": "PMG protects developers from getting compromised by malicious packages",
|
|
4
|
+
"main": "bin/pmg.js",
|
|
5
|
+
"bin": {
|
|
6
|
+
"pmg": "bin/pmg.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"preinstall": "echo \"Installing PMG binary for your platform...\"",
|
|
10
|
+
"postinstall": "node install.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"security",
|
|
14
|
+
"package-manager",
|
|
15
|
+
"malicious-packages",
|
|
16
|
+
"npm",
|
|
17
|
+
"cli",
|
|
18
|
+
"vulnerability",
|
|
19
|
+
"dependency-security",
|
|
20
|
+
"safedep"
|
|
21
|
+
],
|
|
22
|
+
"author": "SafeDep <devops@safedep.io>",
|
|
23
|
+
"license": "Apache-2.0",
|
|
24
|
+
"homepage": "https://github.com/safedep/pmg#readme",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/safedep/pmg.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/safedep/pmg/issues"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=14"
|
|
34
|
+
},
|
|
35
|
+
"os": [
|
|
36
|
+
"darwin",
|
|
37
|
+
"linux",
|
|
38
|
+
"win32"
|
|
39
|
+
],
|
|
40
|
+
"cpu": [
|
|
41
|
+
"x64",
|
|
42
|
+
"arm64",
|
|
43
|
+
"ia32"
|
|
44
|
+
],
|
|
45
|
+
"files": [
|
|
46
|
+
"bin/pmg.js",
|
|
47
|
+
"install.js",
|
|
48
|
+
"config.js",
|
|
49
|
+
"test.js",
|
|
50
|
+
"README.md",
|
|
51
|
+
".npmignore"
|
|
52
|
+
],
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {},
|
|
57
|
+
"version": "0.0.6"
|
|
58
|
+
}
|