@aikidosec/safe-chain 1.4.1 → 1.4.3
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 +119 -13
- package/bin/safe-chain.js +94 -14
- package/docs/troubleshooting.md +85 -13
- package/package.json +3 -1
- package/src/api/aikido.js +85 -27
- package/src/config/configFile.js +22 -0
- package/src/config/environmentVariables.js +19 -0
- package/src/config/settings.js +43 -6
- package/src/installation/downloadAgent.js +125 -0
- package/src/installation/installOnMacOS.js +155 -0
- package/src/installation/installOnWindows.js +203 -0
- package/src/installation/installUltimate.js +35 -0
- package/src/main.js +5 -5
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +28 -2
- package/src/shell-integration/helpers.js +67 -2
- package/src/shell-integration/setup.js +16 -16
- package/src/shell-integration/shellDetection.js +2 -2
- package/src/shell-integration/startup-scripts/init-fish.fish +16 -2
- package/src/shell-integration/startup-scripts/init-posix.sh +8 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +63 -16
- package/src/shell-integration/supported-shells/powershell.js +14 -5
- package/src/shell-integration/supported-shells/windowsPowershell.js +14 -5
- package/src/ultimate/ultimateTroubleshooting.js +111 -0
- package/src/utils/safeSpawn.js +16 -0
package/src/config/settings.js
CHANGED
|
@@ -7,14 +7,20 @@ export const LOGGING_NORMAL = "normal";
|
|
|
7
7
|
export const LOGGING_VERBOSE = "verbose";
|
|
8
8
|
|
|
9
9
|
export function getLoggingLevel() {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
return
|
|
10
|
+
// Priority 1: CLI argument
|
|
11
|
+
const cliLevel = cliArguments.getLoggingLevel();
|
|
12
|
+
if (cliLevel === LOGGING_SILENT || cliLevel === LOGGING_VERBOSE) {
|
|
13
|
+
return cliLevel;
|
|
14
|
+
}
|
|
15
|
+
if (cliLevel) {
|
|
16
|
+
// CLI arg was set but invalid, default to normal for backwards compatibility.
|
|
17
|
+
return LOGGING_NORMAL;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
// Priority 2: Environment variable
|
|
21
|
+
const envLevel = environmentVariables.getLoggingLevel()?.toLowerCase();
|
|
22
|
+
if (envLevel === LOGGING_SILENT || envLevel === LOGGING_VERBOSE) {
|
|
23
|
+
return envLevel;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
return LOGGING_NORMAL;
|
|
@@ -161,3 +167,34 @@ export function getPipCustomRegistries() {
|
|
|
161
167
|
// Normalize each registry (remove protocol if any)
|
|
162
168
|
return uniqueRegistries.map(normalizeRegistry);
|
|
163
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parses comma-separated exclusions from environment variable
|
|
173
|
+
* @param {string | undefined} envValue
|
|
174
|
+
* @returns {string[]}
|
|
175
|
+
*/
|
|
176
|
+
function parseExclusionsFromEnv(envValue) {
|
|
177
|
+
if (!envValue || typeof envValue !== "string") {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return envValue
|
|
182
|
+
.split(",")
|
|
183
|
+
.map((exclusion) => exclusion.trim())
|
|
184
|
+
.filter((exclusion) => exclusion.length > 0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Gets the minimum package age exclusions from both environment variable and config file (merged)
|
|
189
|
+
* @returns {string[]}
|
|
190
|
+
*/
|
|
191
|
+
export function getNpmMinimumPackageAgeExclusions() {
|
|
192
|
+
const envExclusions = parseExclusionsFromEnv(
|
|
193
|
+
environmentVariables.getNpmMinimumPackageAgeExclusions()
|
|
194
|
+
);
|
|
195
|
+
const configExclusions = configFile.getNpmMinimumPackageAgeExclusions();
|
|
196
|
+
|
|
197
|
+
// Merge both sources and remove duplicates
|
|
198
|
+
const allExclusions = [...envExclusions, ...configExclusions];
|
|
199
|
+
return [...new Set(allExclusions)];
|
|
200
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createWriteStream, createReadStream } from "fs";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { pipeline } from "stream/promises";
|
|
4
|
+
import fetch from "make-fetch-happen";
|
|
5
|
+
|
|
6
|
+
const ULTIMATE_VERSION = "v1.0.0";
|
|
7
|
+
|
|
8
|
+
export const DOWNLOAD_URLS = {
|
|
9
|
+
win32: {
|
|
10
|
+
x64: {
|
|
11
|
+
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-amd64.msi`,
|
|
12
|
+
checksum:
|
|
13
|
+
"sha256:c6a36f9b8e55ab6b7e8742cbabc4469d85809237c0f5e6c21af20b36c416ee1d",
|
|
14
|
+
},
|
|
15
|
+
arm64: {
|
|
16
|
+
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-windows-arm64.msi`,
|
|
17
|
+
checksum:
|
|
18
|
+
"sha256:46acd1af6a9938ea194c8ee8b34ca9b47c8de22e088a0791f3c0751dd6239c90",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
darwin: {
|
|
22
|
+
x64: {
|
|
23
|
+
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-amd64.pkg`,
|
|
24
|
+
checksum:
|
|
25
|
+
"sha256:bb1829e8ca422e885baf37bef08dcbe7df7a30f248e2e89c4071564f7d4f3396",
|
|
26
|
+
},
|
|
27
|
+
arm64: {
|
|
28
|
+
url: `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/SafeChainUltimate-darwin-arm64.pkg`,
|
|
29
|
+
checksum:
|
|
30
|
+
"sha256:7fe4a785709911cc366d8224b4c290677573b8c4833bd9054768299e55c5f0ed",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds the download URL for the SafeChain Agent installer.
|
|
37
|
+
* @param {string} fileName
|
|
38
|
+
*/
|
|
39
|
+
export function getAgentDownloadUrl(fileName) {
|
|
40
|
+
return `https://github.com/AikidoSec/safechain-internals/releases/download/${ULTIMATE_VERSION}/${fileName}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Downloads a file from a URL to a local path.
|
|
45
|
+
* @param {string} url
|
|
46
|
+
* @param {string} destPath
|
|
47
|
+
*/
|
|
48
|
+
export async function downloadFile(url, destPath) {
|
|
49
|
+
const response = await fetch(url);
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(`Download failed: ${response.statusText}`);
|
|
52
|
+
}
|
|
53
|
+
await pipeline(response.body, createWriteStream(destPath));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the current agent version.
|
|
58
|
+
*/
|
|
59
|
+
export function getAgentVersion() {
|
|
60
|
+
return ULTIMATE_VERSION;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns download info (url, checksum) for the current OS and architecture.
|
|
65
|
+
* @returns {{ url: string, checksum: string } | null}
|
|
66
|
+
*/
|
|
67
|
+
export function getDownloadInfoForCurrentPlatform() {
|
|
68
|
+
const platform = process.platform;
|
|
69
|
+
const arch = process.arch;
|
|
70
|
+
|
|
71
|
+
if (!Object.hasOwn(DOWNLOAD_URLS, platform)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const platformUrls =
|
|
75
|
+
DOWNLOAD_URLS[/** @type {keyof typeof DOWNLOAD_URLS} */ (platform)];
|
|
76
|
+
|
|
77
|
+
if (!Object.hasOwn(platformUrls, arch)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return platformUrls[/** @type {keyof typeof platformUrls} */ (arch)];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Verifies the checksum of a file.
|
|
86
|
+
* @param {string} filePath
|
|
87
|
+
* @param {string} expectedChecksum - Format: "algorithm:hash" (e.g., "sha256:abc123...")
|
|
88
|
+
* @returns {Promise<boolean>}
|
|
89
|
+
*/
|
|
90
|
+
export async function verifyChecksum(filePath, expectedChecksum) {
|
|
91
|
+
const [algorithm, expected] = expectedChecksum.split(":");
|
|
92
|
+
|
|
93
|
+
const hash = createHash(algorithm);
|
|
94
|
+
|
|
95
|
+
if (filePath.includes("..")) throw new Error("Invalid file path");
|
|
96
|
+
const stream = createReadStream(filePath);
|
|
97
|
+
|
|
98
|
+
for await (const chunk of stream) {
|
|
99
|
+
hash.update(chunk);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const actual = hash.digest("hex");
|
|
103
|
+
return actual === expected;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Downloads the SafeChain agent for the current OS/arch and verifies its checksum.
|
|
108
|
+
* @param {string} fileName - Destination file path
|
|
109
|
+
* @returns {Promise<string | null>} The file path if successful, null if no download URL for current platform
|
|
110
|
+
*/
|
|
111
|
+
export async function downloadAgentToFile(fileName) {
|
|
112
|
+
const info = getDownloadInfoForCurrentPlatform();
|
|
113
|
+
if (!info) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await downloadFile(info.url, fileName);
|
|
118
|
+
|
|
119
|
+
const isValid = await verifyChecksum(fileName, info.checksum);
|
|
120
|
+
if (!isValid) {
|
|
121
|
+
throw new Error("Checksum verification failed");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return fileName;
|
|
125
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { tmpdir } from "os";
|
|
2
|
+
import { unlinkSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { execSync, spawnSync } from "child_process";
|
|
5
|
+
import { ui } from "../environment/userInteraction.js";
|
|
6
|
+
import { printVerboseAndSafeSpawn } from "../utils/safeSpawn.js";
|
|
7
|
+
import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
|
|
10
|
+
const MACOS_PKG_IDENTIFIER = "com.aikidosecurity.safechainultimate";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks if root privileges are available and displays error message if not.
|
|
14
|
+
* @param {string} command - The sudo command to show in the error message
|
|
15
|
+
* @returns {boolean} True if running as root, false otherwise.
|
|
16
|
+
*/
|
|
17
|
+
function requireRootPrivileges(command) {
|
|
18
|
+
if (isRunningAsRoot()) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ui.writeError("Root privileges required.");
|
|
23
|
+
ui.writeInformation("Please run this command with sudo:");
|
|
24
|
+
ui.writeInformation(` ${command}`);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isRunningAsRoot() {
|
|
29
|
+
const rootUserUid = 0;
|
|
30
|
+
return process.getuid?.() === rootUserUid;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function installOnMacOS() {
|
|
34
|
+
if (!requireRootPrivileges("sudo safe-chain ultimate")) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const pkgPath = join(tmpdir(), `SafeChainUltimate-${Date.now()}.pkg`);
|
|
39
|
+
|
|
40
|
+
ui.emptyLine();
|
|
41
|
+
ui.writeInformation(`📥 Downloading SafeChain Ultimate ${getAgentVersion()}`);
|
|
42
|
+
ui.writeVerbose(`Destination: ${pkgPath}`);
|
|
43
|
+
|
|
44
|
+
const result = await downloadAgentToFile(pkgPath);
|
|
45
|
+
if (!result) {
|
|
46
|
+
ui.writeError("No download available for this platform/architecture.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
ui.writeInformation("⚙️ Installing SafeChain Ultimate...");
|
|
52
|
+
await runPkgInstaller(pkgPath);
|
|
53
|
+
|
|
54
|
+
ui.emptyLine();
|
|
55
|
+
ui.writeInformation(
|
|
56
|
+
"✅ SafeChain Ultimate installed and started successfully!",
|
|
57
|
+
);
|
|
58
|
+
ui.emptyLine();
|
|
59
|
+
ui.writeInformation(
|
|
60
|
+
chalk.cyan("🔐 ") +
|
|
61
|
+
chalk.bold("ACTION REQUIRED: ") +
|
|
62
|
+
"macOS will show a popup to install our certificate.",
|
|
63
|
+
);
|
|
64
|
+
ui.writeInformation(
|
|
65
|
+
" " +
|
|
66
|
+
chalk.bold("Please accept the certificate") +
|
|
67
|
+
" to complete the installation.",
|
|
68
|
+
);
|
|
69
|
+
ui.emptyLine();
|
|
70
|
+
} finally {
|
|
71
|
+
ui.writeVerbose(`Cleaning up temporary file: ${pkgPath}`);
|
|
72
|
+
cleanup(pkgPath);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const MACOS_UNINSTALL_SCRIPT =
|
|
77
|
+
"/Library/Application\\ Support/AikidoSecurity/SafeChainUltimate/scripts/uninstall";
|
|
78
|
+
|
|
79
|
+
export async function uninstallOnMacOS() {
|
|
80
|
+
if (!requireRootPrivileges("sudo safe-chain ultimate uninstall")) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ui.emptyLine();
|
|
85
|
+
|
|
86
|
+
if (!isPackageInstalled()) {
|
|
87
|
+
ui.writeInformation("SafeChain Ultimate is not installed.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
ui.writeInformation("🗑️ Uninstalling SafeChain Ultimate...");
|
|
92
|
+
ui.writeVerbose(`Running: ${MACOS_UNINSTALL_SCRIPT}`);
|
|
93
|
+
|
|
94
|
+
const result = spawnSync(MACOS_UNINSTALL_SCRIPT, {
|
|
95
|
+
stdio: "inherit",
|
|
96
|
+
shell: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (result.status !== 0) {
|
|
100
|
+
ui.writeError(
|
|
101
|
+
`Uninstall script failed (exit code: ${result.status}). Please try again or remove manually.`,
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ui.emptyLine();
|
|
107
|
+
ui.writeInformation("✅ SafeChain Ultimate has been uninstalled.");
|
|
108
|
+
ui.emptyLine();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isPackageInstalled() {
|
|
112
|
+
try {
|
|
113
|
+
const output = execSync(`pkgutil --pkg-info ${MACOS_PKG_IDENTIFIER}`, {
|
|
114
|
+
encoding: "utf8",
|
|
115
|
+
stdio: "pipe",
|
|
116
|
+
});
|
|
117
|
+
return output.includes(MACOS_PKG_IDENTIFIER);
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} pkgPath
|
|
125
|
+
*/
|
|
126
|
+
async function runPkgInstaller(pkgPath) {
|
|
127
|
+
// Uses installer to install the package (https://ss64.com/mac/installer.html)
|
|
128
|
+
// Options:
|
|
129
|
+
// -pkg (required): The package to be installed.
|
|
130
|
+
// -target (required): The target volume is specified with the -target parameter.
|
|
131
|
+
// --> "-target /" installs to the current boot volume.
|
|
132
|
+
|
|
133
|
+
const result = await printVerboseAndSafeSpawn(
|
|
134
|
+
"installer",
|
|
135
|
+
["-pkg", pkgPath, "-target", "/"],
|
|
136
|
+
{
|
|
137
|
+
stdio: "inherit",
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (result.status !== 0) {
|
|
142
|
+
throw new Error(`PKG installer failed (exit code: ${result.status})`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {string} pkgPath
|
|
148
|
+
*/
|
|
149
|
+
function cleanup(pkgPath) {
|
|
150
|
+
try {
|
|
151
|
+
unlinkSync(pkgPath);
|
|
152
|
+
} catch {
|
|
153
|
+
ui.writeVerbose("Failed to clean up temporary installer file.");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { tmpdir } from "os";
|
|
2
|
+
import { unlinkSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { ui } from "../environment/userInteraction.js";
|
|
6
|
+
import { printVerboseAndSafeSpawn, safeSpawn } from "../utils/safeSpawn.js";
|
|
7
|
+
import { downloadAgentToFile, getAgentVersion } from "./downloadAgent.js";
|
|
8
|
+
|
|
9
|
+
const WINDOWS_SERVICE_NAME = "SafeChainUltimate";
|
|
10
|
+
const WINDOWS_APP_NAME = "SafeChain Ultimate";
|
|
11
|
+
|
|
12
|
+
export async function uninstallOnWindows() {
|
|
13
|
+
if (!(await requireAdminPrivileges())) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
ui.emptyLine();
|
|
18
|
+
|
|
19
|
+
const productCode = getInstalledProductCode();
|
|
20
|
+
if (!productCode) {
|
|
21
|
+
ui.writeInformation("SafeChain Ultimate is not installed.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await stopServiceIfRunning();
|
|
26
|
+
|
|
27
|
+
ui.writeInformation("🗑️ Uninstalling SafeChain Ultimate...");
|
|
28
|
+
await uninstallByProductCode(productCode);
|
|
29
|
+
|
|
30
|
+
ui.emptyLine();
|
|
31
|
+
ui.writeInformation("✅ SafeChain Ultimate has been uninstalled.");
|
|
32
|
+
ui.emptyLine();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function installOnWindows() {
|
|
36
|
+
if (!(await requireAdminPrivileges())) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const msiPath = join(tmpdir(), `SafeChainUltimate-${Date.now()}.msi`);
|
|
41
|
+
|
|
42
|
+
ui.emptyLine();
|
|
43
|
+
ui.writeInformation(`📥 Downloading SafeChain Ultimate ${getAgentVersion()}`);
|
|
44
|
+
ui.writeVerbose(`Destination: ${msiPath}`);
|
|
45
|
+
|
|
46
|
+
const result = await downloadAgentToFile(msiPath);
|
|
47
|
+
if (!result) {
|
|
48
|
+
ui.writeError("No download available for this platform/architecture.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
ui.emptyLine();
|
|
54
|
+
await stopServiceIfRunning();
|
|
55
|
+
await uninstallIfInstalled();
|
|
56
|
+
|
|
57
|
+
ui.writeInformation("⚙️ Installing SafeChain Ultimate...");
|
|
58
|
+
await runMsiInstaller(msiPath);
|
|
59
|
+
|
|
60
|
+
ui.emptyLine();
|
|
61
|
+
ui.writeInformation(
|
|
62
|
+
"✅ SafeChain Ultimate installed and started successfully!",
|
|
63
|
+
);
|
|
64
|
+
ui.emptyLine();
|
|
65
|
+
} finally {
|
|
66
|
+
ui.writeVerbose(`Cleaning up temporary file: ${msiPath}`);
|
|
67
|
+
cleanup(msiPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checks if admin privileges are available and displays error message if not.
|
|
73
|
+
* @returns {Promise<boolean>} True if running as admin, false otherwise.
|
|
74
|
+
*/
|
|
75
|
+
async function requireAdminPrivileges() {
|
|
76
|
+
if (await isRunningAsAdmin()) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ui.writeError("Administrator privileges required.");
|
|
81
|
+
ui.writeInformation(
|
|
82
|
+
"Please run this command in an elevated terminal (Run as Administrator).",
|
|
83
|
+
);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function isRunningAsAdmin() {
|
|
88
|
+
// Uses Windows Security API to check if current process has admin privileges.
|
|
89
|
+
// Returns "True" or "False" as a string.
|
|
90
|
+
const result = await safeSpawn(
|
|
91
|
+
"powershell",
|
|
92
|
+
[
|
|
93
|
+
"-Command",
|
|
94
|
+
"([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)",
|
|
95
|
+
],
|
|
96
|
+
{ stdio: "pipe" },
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return result.status === 0 && result.stdout.trim() === "True";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns the MSI product code for SafeChain Ultimate, or null if not installed.
|
|
104
|
+
* @returns {string | null}
|
|
105
|
+
*/
|
|
106
|
+
function getInstalledProductCode() {
|
|
107
|
+
// Query Win32_Product via WMI to find the installed SafeChain Agent.
|
|
108
|
+
// If found, outputs the product GUID (e.g., "{12345678-1234-...}") needed for msiexec uninstall.
|
|
109
|
+
ui.writeVerbose(`Finding product code with PowerShell`);
|
|
110
|
+
|
|
111
|
+
let productCode;
|
|
112
|
+
try {
|
|
113
|
+
productCode = execSync(
|
|
114
|
+
`powershell -Command "$app = Get-WmiObject -Class Win32_Product -Filter \\"Name='${WINDOWS_APP_NAME}'\\"; if ($app) { Write-Output $app.IdentifyingNumber }"`,
|
|
115
|
+
{ encoding: "utf8" },
|
|
116
|
+
).trim();
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return productCode || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} productCode
|
|
125
|
+
*/
|
|
126
|
+
async function uninstallByProductCode(productCode) {
|
|
127
|
+
ui.writeVerbose(`Found product code: ${productCode}`);
|
|
128
|
+
|
|
129
|
+
// Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec)
|
|
130
|
+
// Options:
|
|
131
|
+
// - /x: Uninstalls the package.
|
|
132
|
+
// - /qn: Specifies there's no UI during the installation process.
|
|
133
|
+
// - /norestart: Stops the device from restarting after the installation completes.
|
|
134
|
+
const uninstallResult = await printVerboseAndSafeSpawn(
|
|
135
|
+
"msiexec",
|
|
136
|
+
["/x", productCode, "/qn", "/norestart"],
|
|
137
|
+
{ stdio: "inherit" },
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (uninstallResult.status !== 0) {
|
|
141
|
+
throw new Error(`Uninstall failed (exit code: ${uninstallResult.status})`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function uninstallIfInstalled() {
|
|
146
|
+
const productCode = getInstalledProductCode();
|
|
147
|
+
if (!productCode) {
|
|
148
|
+
ui.writeVerbose("No existing installation found (fresh install).");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
ui.writeInformation("🗑️ Removing previous installation...");
|
|
153
|
+
await uninstallByProductCode(productCode);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} msiPath
|
|
158
|
+
*/
|
|
159
|
+
async function runMsiInstaller(msiPath) {
|
|
160
|
+
// Use msiexec to run the msi installer quitely (https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec)
|
|
161
|
+
// Options:
|
|
162
|
+
// - /i: Specifies normal installation
|
|
163
|
+
// - /qn: Specifies there's no UI during the installation process.
|
|
164
|
+
|
|
165
|
+
const result = await printVerboseAndSafeSpawn(
|
|
166
|
+
"msiexec",
|
|
167
|
+
["/i", msiPath, "/qn"],
|
|
168
|
+
{
|
|
169
|
+
stdio: "inherit",
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (result.status !== 0) {
|
|
174
|
+
throw new Error(`MSI installer failed (exit code: ${result.status})`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function stopServiceIfRunning() {
|
|
179
|
+
ui.writeInformation("⏹️ Stopping running service...");
|
|
180
|
+
|
|
181
|
+
const result = await printVerboseAndSafeSpawn(
|
|
182
|
+
"net",
|
|
183
|
+
["stop", WINDOWS_SERVICE_NAME],
|
|
184
|
+
{
|
|
185
|
+
stdio: "pipe",
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (result.status !== 0) {
|
|
190
|
+
ui.writeVerbose("Service not running (will start after installation).");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {string} msiPath
|
|
196
|
+
*/
|
|
197
|
+
function cleanup(msiPath) {
|
|
198
|
+
try {
|
|
199
|
+
unlinkSync(msiPath);
|
|
200
|
+
} catch {
|
|
201
|
+
ui.writeVerbose("Failed to clean up temporary installer file.");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { platform } from "os";
|
|
2
|
+
import { ui } from "../environment/userInteraction.js";
|
|
3
|
+
import { initializeCliArguments } from "../config/cliArguments.js";
|
|
4
|
+
import { installOnWindows, uninstallOnWindows } from "./installOnWindows.js";
|
|
5
|
+
import { installOnMacOS, uninstallOnMacOS } from "./installOnMacOS.js";
|
|
6
|
+
|
|
7
|
+
export async function uninstallUltimate() {
|
|
8
|
+
initializeCliArguments(process.argv);
|
|
9
|
+
|
|
10
|
+
const operatingSystem = platform();
|
|
11
|
+
|
|
12
|
+
if (operatingSystem === "win32") {
|
|
13
|
+
await uninstallOnWindows();
|
|
14
|
+
} else if (operatingSystem === "darwin") {
|
|
15
|
+
await uninstallOnMacOS();
|
|
16
|
+
} else {
|
|
17
|
+
ui.writeInformation(
|
|
18
|
+
`Uninstall is not yet supported on ${operatingSystem}.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function installUltimate() {
|
|
24
|
+
const operatingSystem = platform();
|
|
25
|
+
|
|
26
|
+
if (operatingSystem === "win32") {
|
|
27
|
+
await installOnWindows();
|
|
28
|
+
} else if (operatingSystem === "darwin") {
|
|
29
|
+
await installOnMacOS();
|
|
30
|
+
} else {
|
|
31
|
+
ui.writeInformation(
|
|
32
|
+
`${operatingSystem} is not supported yet by SafeChain's ultimate version.`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/main.js
CHANGED
|
@@ -73,20 +73,20 @@ export async function main(args) {
|
|
|
73
73
|
ui.writeVerbose(
|
|
74
74
|
`${chalk.green("✔")} Safe-chain: Scanned ${
|
|
75
75
|
auditStats.totalPackages
|
|
76
|
-
} packages, no malware found
|
|
76
|
+
} packages, no malware found.`,
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
if (proxy.hasSuppressedVersions()) {
|
|
81
81
|
ui.writeInformation(
|
|
82
82
|
`${chalk.yellow(
|
|
83
|
-
"ℹ"
|
|
84
|
-
)} Safe-chain: Some package versions were suppressed due to minimum age requirement
|
|
83
|
+
"ℹ",
|
|
84
|
+
)} Safe-chain: Some package versions were suppressed due to minimum age requirement.`,
|
|
85
85
|
);
|
|
86
86
|
ui.writeInformation(
|
|
87
87
|
` To disable this check, use: ${chalk.cyan(
|
|
88
|
-
"--safe-chain-skip-minimum-package-age"
|
|
89
|
-
)}
|
|
88
|
+
"--safe-chain-skip-minimum-package-age",
|
|
89
|
+
)}`,
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getMinimumPackageAgeHours } from "../../../config/settings.js";
|
|
1
|
+
import { getMinimumPackageAgeHours, getNpmMinimumPackageAgeExclusions } from "../../../config/settings.js";
|
|
2
2
|
import { ui } from "../../../environment/userInteraction.js";
|
|
3
3
|
import { getHeaderValueAsString } from "../../http-utils.js";
|
|
4
4
|
|
|
@@ -65,6 +65,16 @@ export function modifyNpmInfoResponse(body, headers) {
|
|
|
65
65
|
return body;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Check if this package is excluded from minimum age filtering
|
|
69
|
+
const packageName = bodyJson.name;
|
|
70
|
+
const exclusions = getNpmMinimumPackageAgeExclusions();
|
|
71
|
+
if (packageName && exclusions.some((pattern) => matchesExclusionPattern(packageName, pattern))) {
|
|
72
|
+
ui.writeVerbose(
|
|
73
|
+
`Safe-chain: ${packageName} is excluded from minimum package age filtering (minimumPackageAgeExclusions setting).`
|
|
74
|
+
);
|
|
75
|
+
return body;
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
const cutOff = new Date(
|
|
69
79
|
new Date().getTime() - getMinimumPackageAgeHours() * 3600 * 1000
|
|
70
80
|
);
|
|
@@ -116,8 +126,10 @@ export function modifyNpmInfoResponse(body, headers) {
|
|
|
116
126
|
function deleteVersionFromJson(json, version) {
|
|
117
127
|
state.hasSuppressedVersions = true;
|
|
118
128
|
|
|
129
|
+
const packageName = typeof json?.name === "string" ? json.name : "(unknown)";
|
|
130
|
+
|
|
119
131
|
ui.writeVerbose(
|
|
120
|
-
`Safe-chain: ${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).`
|
|
132
|
+
`Safe-chain: ${packageName}@${version} is newer than ${getMinimumPackageAgeHours()} hours and was removed (minimumPackageAgeInHours setting).`
|
|
121
133
|
);
|
|
122
134
|
|
|
123
135
|
delete json.time[version];
|
|
@@ -175,3 +187,17 @@ function getMostRecentTag(tagList) {
|
|
|
175
187
|
export function getHasSuppressedVersions() {
|
|
176
188
|
return state.hasSuppressedVersions;
|
|
177
189
|
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Checks if a package name matches an exclusion pattern.
|
|
193
|
+
* Supports trailing wildcard (*) for prefix matching.
|
|
194
|
+
* @param {string} packageName
|
|
195
|
+
* @param {string} pattern
|
|
196
|
+
* @returns {boolean}
|
|
197
|
+
*/
|
|
198
|
+
function matchesExclusionPattern(packageName, pattern) {
|
|
199
|
+
if (pattern.endsWith("/*")) {
|
|
200
|
+
return packageName.startsWith(pattern.slice(0, -1));
|
|
201
|
+
}
|
|
202
|
+
return packageName === pattern;
|
|
203
|
+
}
|