@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 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
+ }