diff --git a/README.md b/README.md index 14dc26c..57d1bf4 100644 --- a/README.md +++ b/README.md @@ -152,23 +152,36 @@ iex (iwr "https://github.com/AikidoSec/safe-chain/releases/latest/download/unins ## Logging -You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag: +You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag or the `SAFE_CHAIN_LOGGING` environment variable. -- `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit. +### Configuration Options - Example usage: +You can set the logging level through multiple sources (in order of priority): - ```shell - npm install express --safe-chain-logging=silent - ``` +1. **CLI Argument** (highest priority): -- `--safe-chain-logging=verbose` - Enables detailed diagnostic output from Aikido Safe Chain. Useful for troubleshooting issues or understanding what Safe Chain is doing behind the scenes. + - `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit. - Example usage: + ```shell + npm install express --safe-chain-logging=silent + ``` - ```shell - npm install express --safe-chain-logging=verbose - ``` + - `--safe-chain-logging=verbose` - Enables detailed diagnostic output from Aikido Safe Chain. Useful for troubleshooting issues or understanding what Safe Chain is doing behind the scenes. + + ```shell + npm install express --safe-chain-logging=verbose + ``` + +2. **Environment Variable**: + + ```shell + export SAFE_CHAIN_LOGGING=verbose + npm install express + ``` + + Valid values: `silent`, `normal`, `verbose` + + This is useful for setting a default logging level for all package manager commands in your terminal session or CI/CD environment. ## Minimum Package Age diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 8c32bee..0cd6098 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -48,12 +48,16 @@ These test packages are flagged as malware and should be blocked by Safe Chain. ### Logging Options -Use logging flags to get more information: +Use logging flags or environment variables to get more information: ```bash # Verbose mode - detailed diagnostic output for troubleshooting npm install express --safe-chain-logging=verbose +# Or set it globally for all commands in your session +export SAFE_CHAIN_LOGGING=verbose +npm install express + # Silent mode - suppress all output except malware blocking npm install express --safe-chain-logging=silent ``` @@ -277,11 +281,16 @@ rm -rf ~/.safe-chain ### Enable Verbose Logging -Get detailed diagnostic output: +Get detailed diagnostic output using a CLI flag or environment variable: ```bash +# Using CLI flag npm install express --safe-chain-logging=verbose pip install requests --safe-chain-logging=verbose + +# Using environment variable (applies to all commands) +export SAFE_CHAIN_LOGGING=verbose +npm install express ``` ### Report Issues diff --git a/packages/safe-chain/src/config/environmentVariables.js b/packages/safe-chain/src/config/environmentVariables.js index 64da107..1b85ed7 100644 --- a/packages/safe-chain/src/config/environmentVariables.js +++ b/packages/safe-chain/src/config/environmentVariables.js @@ -25,3 +25,12 @@ export function getNpmCustomRegistries() { export function getPipCustomRegistries() { return process.env.SAFE_CHAIN_PIP_CUSTOM_REGISTRIES; } + +/** + * Gets the logging level from environment variable + * Valid values: "silent", "normal", "verbose" + * @returns {string | undefined} + */ +export function getLoggingLevel() { + return process.env.SAFE_CHAIN_LOGGING; +} diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 573c3ab..6910fe3 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -7,14 +7,20 @@ export const LOGGING_NORMAL = "normal"; export const LOGGING_VERBOSE = "verbose"; export function getLoggingLevel() { - const level = cliArguments.getLoggingLevel(); - - if (level === LOGGING_SILENT) { - return LOGGING_SILENT; + // Priority 1: CLI argument + const cliLevel = cliArguments.getLoggingLevel(); + if (cliLevel === LOGGING_SILENT || cliLevel === LOGGING_VERBOSE) { + return cliLevel; + } + if (cliLevel) { + // CLI arg was set but invalid, default to normal for backwards compatibility. + return LOGGING_NORMAL; } - if (level === LOGGING_VERBOSE) { - return LOGGING_VERBOSE; + // Priority 2: Environment variable + const envLevel = environmentVariables.getLoggingLevel()?.toLowerCase(); + if (envLevel === LOGGING_SILENT || envLevel === LOGGING_VERBOSE) { + return envLevel; } return LOGGING_NORMAL; diff --git a/packages/safe-chain/src/config/settings.spec.js b/packages/safe-chain/src/config/settings.spec.js index db513f3..314fac0 100644 --- a/packages/safe-chain/src/config/settings.spec.js +++ b/packages/safe-chain/src/config/settings.spec.js @@ -11,9 +11,15 @@ mock.module("fs", { }, }); -const { getNpmCustomRegistries, getPipCustomRegistries } = await import( - "./settings.js" -); +const { + getNpmCustomRegistries, + getPipCustomRegistries, + getLoggingLevel, + LOGGING_SILENT, + LOGGING_NORMAL, + LOGGING_VERBOSE, +} = await import("./settings.js"); +const { initializeCliArguments } = await import("./cliArguments.js"); for (const { packageManager, getCustomRegistries, envVarName } of [ { @@ -26,8 +32,7 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ getCustomRegistries: getPipCustomRegistries, envVarName: "SAFE_CHAIN_PIP_CUSTOM_REGISTRIES", }, -]) -{ +]) { describe(getCustomRegistries.name, async () => { let originalEnv; @@ -55,7 +60,10 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ it("should return registries without protocol", () => { configFileContent = JSON.stringify({ [packageManager]: { - customRegistries: [`${packageManager}.company.com`, "registry.internal.net"], + customRegistries: [ + `${packageManager}.company.com`, + "registry.internal.net", + ], }, }); @@ -143,8 +151,7 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ it("should parse comma-separated registries from environment variable", () => { delete process.env[envVarName]; - process.env[envVarName] = - "env1.registry.com,env2.registry.net"; + process.env[envVarName] = "env1.registry.com,env2.registry.net"; configFileContent = undefined; const registries = getCustomRegistries(); @@ -157,8 +164,7 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ it("should trim whitespace from environment variable registries", () => { delete process.env[envVarName]; - process.env[envVarName] = - " env1.registry.com , env2.registry.net "; + process.env[envVarName] = " env1.registry.com , env2.registry.net "; configFileContent = undefined; const registries = getCustomRegistries(); @@ -188,11 +194,15 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ it("should remove duplicate registries when merging env and config", () => { delete process.env[envVarName]; - process.env[envVarName] = - `${packageManager}.company.com,env.registry.com`; + process.env[ + envVarName + ] = `${packageManager}.company.com,env.registry.com`; configFileContent = JSON.stringify({ [packageManager]: { - customRegistries: [`${packageManager}.company.com`, "config.registry.net"], + customRegistries: [ + `${packageManager}.company.com`, + "config.registry.net", + ], }, }); @@ -221,8 +231,7 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ it("should handle empty strings in comma-separated list", () => { delete process.env[envVarName]; - process.env[envVarName] = - "env1.registry.com,,env2.registry.net,"; + process.env[envVarName] = "env1.registry.com,,env2.registry.net,"; configFileContent = undefined; const registries = getCustomRegistries(); @@ -264,3 +273,95 @@ for (const { packageManager, getCustomRegistries, envVarName } of [ }); }); } + +describe("getLoggingLevel", () => { + let originalEnv; + + beforeEach(() => { + originalEnv = process.env.SAFE_CHAIN_LOGGING; + delete process.env.SAFE_CHAIN_LOGGING; + // Reset CLI arguments state + initializeCliArguments([]); + }); + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.SAFE_CHAIN_LOGGING = originalEnv; + } else { + delete process.env.SAFE_CHAIN_LOGGING; + } + }); + + it("should return normal by default when nothing is configured", () => { + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_NORMAL); + }); + + it("should return silent from environment variable", () => { + process.env.SAFE_CHAIN_LOGGING = "silent"; + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_SILENT); + }); + + it("should return verbose from environment variable", () => { + process.env.SAFE_CHAIN_LOGGING = "verbose"; + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_VERBOSE); + }); + + it("should handle uppercase environment variable values", () => { + process.env.SAFE_CHAIN_LOGGING = "VERBOSE"; + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_VERBOSE); + }); + + it("should handle mixed case environment variable values", () => { + process.env.SAFE_CHAIN_LOGGING = "Silent"; + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_SILENT); + }); + + it("should return normal for invalid environment variable values", () => { + process.env.SAFE_CHAIN_LOGGING = "invalid"; + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_NORMAL); + }); + + it("should prioritize CLI argument over environment variable", () => { + process.env.SAFE_CHAIN_LOGGING = "verbose"; + initializeCliArguments(["--safe-chain-logging=silent"]); + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_SILENT); + }); + + it("should use environment variable when CLI argument is not set", () => { + process.env.SAFE_CHAIN_LOGGING = "silent"; + initializeCliArguments(["install", "express"]); + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_SILENT); + }); + + it("should return normal when CLI argument is invalid (even if env var is valid)", () => { + process.env.SAFE_CHAIN_LOGGING = "verbose"; + initializeCliArguments(["--safe-chain-logging=invalid"]); + + const level = getLoggingLevel(); + + assert.strictEqual(level, LOGGING_NORMAL); + }); +});