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,24 +152,37 @@ 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.
### Configuration Options
You can set the logging level through multiple sources (in order of priority):
1. **CLI Argument** (highest priority):
- `--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
```
- `--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.
Example usage:
```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
You can configure how long packages must exist before Safe Chain allows their installation. By default, packages must be at least 24 hours old before they can be installed through npm-based package managers.

View file

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

View file

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

View file

@ -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;

View file

@ -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);
});
});