@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.
@@ -7,14 +7,20 @@ export const LOGGING_NORMAL = "normal";
7
7
  export const LOGGING_VERBOSE = "verbose";
8
8
 
9
9
  export function getLoggingLevel() {
10
- const level = cliArguments.getLoggingLevel();
11
-
12
- if (level === LOGGING_SILENT) {
13
- return LOGGING_SILENT;
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
- if (level === LOGGING_VERBOSE) {
17
- return LOGGING_VERBOSE;
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
+ }