Merge pull request #285 from AikidoSec/logging-as-env-variable

Allow to configure loglevel through an env variable
This commit is contained in:
Sander Declerck 2026-01-12 12:41:13 +01:00 committed by GitHub
commit 31b5f73197
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 172 additions and 34 deletions

View file

@ -152,23 +152,36 @@ iex (iwr "https://github.com/AikidoSec/safe-chain/releases/latest/download/unins
## Logging ## 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 1. **CLI Argument** (highest priority):
npm install express --safe-chain-logging=silent
```
- `--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 - `--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.
npm install express --safe-chain-logging=verbose
``` ```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 ## Minimum Package Age

View file

@ -48,12 +48,16 @@ These test packages are flagged as malware and should be blocked by Safe Chain.
### Logging Options ### Logging Options
Use logging flags to get more information: Use logging flags or environment variables to get more information:
```bash ```bash
# Verbose mode - detailed diagnostic output for troubleshooting # Verbose mode - detailed diagnostic output for troubleshooting
npm install express --safe-chain-logging=verbose 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 # Silent mode - suppress all output except malware blocking
npm install express --safe-chain-logging=silent npm install express --safe-chain-logging=silent
``` ```
@ -277,11 +281,16 @@ rm -rf ~/.safe-chain
### Enable Verbose Logging ### Enable Verbose Logging
Get detailed diagnostic output: Get detailed diagnostic output using a CLI flag or environment variable:
```bash ```bash
# Using CLI flag
npm install express --safe-chain-logging=verbose npm install express --safe-chain-logging=verbose
pip install requests --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 ### Report Issues

View file

@ -25,3 +25,12 @@ export function getNpmCustomRegistries() {
export function getPipCustomRegistries() { export function getPipCustomRegistries() {
return process.env.SAFE_CHAIN_PIP_CUSTOM_REGISTRIES; 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;
}

View file

@ -7,14 +7,20 @@ export const LOGGING_NORMAL = "normal";
export const LOGGING_VERBOSE = "verbose"; export const LOGGING_VERBOSE = "verbose";
export function getLoggingLevel() { export function getLoggingLevel() {
const level = cliArguments.getLoggingLevel(); // Priority 1: CLI argument
const cliLevel = cliArguments.getLoggingLevel();
if (level === LOGGING_SILENT) { if (cliLevel === LOGGING_SILENT || cliLevel === LOGGING_VERBOSE) {
return LOGGING_SILENT; return cliLevel;
}
if (cliLevel) {
// CLI arg was set but invalid, default to normal for backwards compatibility.
return LOGGING_NORMAL;
} }
if (level === LOGGING_VERBOSE) { // Priority 2: Environment variable
return LOGGING_VERBOSE; const envLevel = environmentVariables.getLoggingLevel()?.toLowerCase();
if (envLevel === LOGGING_SILENT || envLevel === LOGGING_VERBOSE) {
return envLevel;
} }
return LOGGING_NORMAL; return LOGGING_NORMAL;

View file

@ -11,9 +11,15 @@ mock.module("fs", {
}, },
}); });
const { getNpmCustomRegistries, getPipCustomRegistries } = await import( const {
"./settings.js" 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 [ for (const { packageManager, getCustomRegistries, envVarName } of [
{ {
@ -26,8 +32,7 @@ for (const { packageManager, getCustomRegistries, envVarName } of [
getCustomRegistries: getPipCustomRegistries, getCustomRegistries: getPipCustomRegistries,
envVarName: "SAFE_CHAIN_PIP_CUSTOM_REGISTRIES", envVarName: "SAFE_CHAIN_PIP_CUSTOM_REGISTRIES",
}, },
]) ]) {
{
describe(getCustomRegistries.name, async () => { describe(getCustomRegistries.name, async () => {
let originalEnv; let originalEnv;
@ -55,7 +60,10 @@ for (const { packageManager, getCustomRegistries, envVarName } of [
it("should return registries without protocol", () => { it("should return registries without protocol", () => {
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
[packageManager]: { [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", () => { it("should parse comma-separated registries from environment variable", () => {
delete process.env[envVarName]; delete process.env[envVarName];
process.env[envVarName] = process.env[envVarName] = "env1.registry.com,env2.registry.net";
"env1.registry.com,env2.registry.net";
configFileContent = undefined; configFileContent = undefined;
const registries = getCustomRegistries(); const registries = getCustomRegistries();
@ -157,8 +164,7 @@ for (const { packageManager, getCustomRegistries, envVarName } of [
it("should trim whitespace from environment variable registries", () => { it("should trim whitespace from environment variable registries", () => {
delete process.env[envVarName]; delete process.env[envVarName];
process.env[envVarName] = process.env[envVarName] = " env1.registry.com , env2.registry.net ";
" env1.registry.com , env2.registry.net ";
configFileContent = undefined; configFileContent = undefined;
const registries = getCustomRegistries(); const registries = getCustomRegistries();
@ -188,11 +194,15 @@ for (const { packageManager, getCustomRegistries, envVarName } of [
it("should remove duplicate registries when merging env and config", () => { it("should remove duplicate registries when merging env and config", () => {
delete process.env[envVarName]; delete process.env[envVarName];
process.env[envVarName] = process.env[
`${packageManager}.company.com,env.registry.com`; envVarName
] = `${packageManager}.company.com,env.registry.com`;
configFileContent = JSON.stringify({ configFileContent = JSON.stringify({
[packageManager]: { [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", () => { it("should handle empty strings in comma-separated list", () => {
delete process.env[envVarName]; delete process.env[envVarName];
process.env[envVarName] = process.env[envVarName] = "env1.registry.com,,env2.registry.net,";
"env1.registry.com,,env2.registry.net,";
configFileContent = undefined; configFileContent = undefined;
const registries = getCustomRegistries(); 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);
});
});