Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,45 @@ You can set the logging level through multiple sources (in order of priority):

This is useful for setting a default logging level for all package manager commands in your terminal session or CI/CD environment.

## File Logging

You can mirror Aikido Safe Chain output to a log file using the `--safe-chain-log-file` flag or the `SAFE_CHAIN_LOG_FILE` environment variable. File logging is disabled by default and enabled when a path is set. The file format (`--safe-chain-log-file-format`) and verbosity (`--safe-chain-log-file-verbosity`) are controlled independently from the terminal output.

### Configuration Options

Set through any of these (in order of priority):

1. **CLI Argument** (highest priority):

```shell
npm install express \
--safe-chain-log-file=~/safe-chain.log \
--safe-chain-log-file-format=plain \
--safe-chain-log-file-verbosity=normal
```

2. **Environment Variable**:

```shell
export SAFE_CHAIN_LOG_FILE=~/safe-chain.log
export SAFE_CHAIN_LOG_FILE_FORMAT=plain
export SAFE_CHAIN_LOG_FILE_VERBOSITY=normal
```

3. **Config File** (`~/.safe-chain/config.json`):

```json
{
"logFile": "~/safe-chain.log",
"logFileFormat": "plain",
"logFileVerbosity": "normal"
}
```

`logFileFormat` — `json` (default) or `plain`.

`logFileVerbosity` — `silent`, `normal`, or `verbose` (default). Independent from `--safe-chain-logging`.

## Minimum Package Age

You can configure how long packages must exist before Safe Chain allows their installation. By default, packages must be at least 48 hours old before they can be installed.
Expand Down
71 changes: 70 additions & 1 deletion packages/safe-chain/src/config/cliArguments.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { ui } from "../environment/userInteraction.js";

/**
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, malwareListBaseUrl: string | undefined}}
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, malwareListBaseUrl: string | undefined, logFile: string | undefined, logFileFormat: string | undefined, logFileVerbosity: string | undefined}}
*/
const state = {
loggingLevel: undefined,
skipMinimumPackageAge: undefined,
minimumPackageAgeHours: undefined,
malwareListBaseUrl: undefined,
logFile: undefined,
logFileFormat: undefined,
logFileVerbosity: undefined,
};

const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
Expand All @@ -22,6 +25,9 @@ export function initializeCliArguments(args) {
state.skipMinimumPackageAge = undefined;
state.minimumPackageAgeHours = undefined;
state.malwareListBaseUrl = undefined;
state.logFile = undefined;
state.logFileFormat = undefined;
state.logFileVerbosity = undefined;

const safeChainArgs = [];
const remainingArgs = [];
Expand All @@ -38,6 +44,9 @@ export function initializeCliArguments(args) {
setSkipMinimumPackageAge(safeChainArgs);
setMinimumPackageAgeHours(safeChainArgs);
setMalwareListBaseUrl(safeChainArgs);
setLogFile(safeChainArgs);
setLogFileFormat(safeChainArgs);
setLogFileVerbosity(safeChainArgs);
checkDeprecatedPythonFlag(args);
return remainingArgs;
}
Expand Down Expand Up @@ -132,6 +141,66 @@ export function getMalwareListBaseUrl() {
return state.malwareListBaseUrl;
}

/**
* @param {string[]} args
* @returns {void}
*/
function setLogFile(args) {
const argName = SAFE_CHAIN_ARG_PREFIX + "log-file=";

const value = getLastArgEqualsValue(args, argName);
if (value) {
state.logFile = value;
}
}

/**
* @returns {string | undefined}
*/
export function getLogFile() {
return state.logFile;
}

/**
* @param {string[]} args
* @returns {void}
*/
function setLogFileFormat(args) {
const argName = SAFE_CHAIN_ARG_PREFIX + "log-file-format=";

const value = getLastArgEqualsValue(args, argName);
if (value) {
state.logFileFormat = value.toLowerCase();
}
}

/**
* @returns {string | undefined}
*/
export function getLogFileFormat() {
return state.logFileFormat;
}

/**
* @param {string[]} args
* @returns {void}
*/
function setLogFileVerbosity(args) {
const argName = SAFE_CHAIN_ARG_PREFIX + "log-file-verbosity=";

const value = getLastArgEqualsValue(args, argName);
if (value) {
state.logFileVerbosity = value.toLowerCase();
}
}

/**
* @returns {string | undefined}
*/
export function getLogFileVerbosity() {
return state.logFileVerbosity;
}

/**
* @param {string[]} args
* @param {string} flagName
Expand Down
139 changes: 139 additions & 0 deletions packages/safe-chain/src/config/cliArguments.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
getLoggingLevel,
getSkipMinimumPackageAge,
getMinimumPackageAgeHours,
getLogFile,
getLogFileFormat,
getLogFileVerbosity,
} from "./cliArguments.js";
import { ui } from "../environment/userInteraction.js";

Expand Down Expand Up @@ -308,4 +311,140 @@ describe("initializeCliArguments", () => {
ui.writeWarning = originalWriteWarning;
}
});

it("should not set logFile when no log-file argument is passed", () => {
initializeCliArguments(["install", "express"]);

assert.strictEqual(getLogFile(), undefined);
});

it("should parse log-file value and set state", () => {
const args = ["--safe-chain-log-file=/tmp/safe-chain.log", "install"];
const result = initializeCliArguments(args);

assert.deepEqual(result, ["install"]);
assert.strictEqual(getLogFile(), "/tmp/safe-chain.log");
});

it("should use the last log-file argument when multiple are provided", () => {
const args = [
"--safe-chain-log-file=/tmp/first.log",
"--safe-chain-log-file=/tmp/second.log",
"install",
];
initializeCliArguments(args);

assert.strictEqual(getLogFile(), "/tmp/second.log");
});

it("should handle log-file with other safe-chain arguments", () => {
const args = [
"--safe-chain-logging=verbose",
"--safe-chain-log-file=/tmp/test.log",
"install",
"lodash",
];
const result = initializeCliArguments(args);

assert.deepEqual(result, ["install", "lodash"]);
assert.strictEqual(getLoggingLevel(), "verbose");
assert.strictEqual(getLogFile(), "/tmp/test.log");
});

it("should reset logFile between calls", () => {
initializeCliArguments(["--safe-chain-log-file=/tmp/test.log", "install"]);
assert.strictEqual(getLogFile(), "/tmp/test.log");

initializeCliArguments(["install"]);
assert.strictEqual(getLogFile(), undefined);
});

it("should not set logFileFormat when no log-file-format argument is passed", () => {
initializeCliArguments(["install", "express"]);

assert.strictEqual(getLogFileFormat(), undefined);
});

it("should parse log-file-format=json and set state", () => {
const args = ["--safe-chain-log-file-format=json", "install"];
const result = initializeCliArguments(args);

assert.deepEqual(result, ["install"]);
assert.strictEqual(getLogFileFormat(), "json");
});

it("should parse log-file-format=plain and set state", () => {
const args = ["--safe-chain-log-file-format=plain", "install"];
initializeCliArguments(args);

assert.strictEqual(getLogFileFormat(), "plain");
});

it("should handle log-file-format case-insensitively", () => {
initializeCliArguments(["--safe-chain-log-file-format=JSON", "install"]);

assert.strictEqual(getLogFileFormat(), "json");
});

it("should use the last log-file-format argument when multiple are provided", () => {
const args = [
"--safe-chain-log-file-format=plain",
"--safe-chain-log-file-format=json",
"install",
];
initializeCliArguments(args);

assert.strictEqual(getLogFileFormat(), "json");
});

it("should reset logFileFormat between calls", () => {
initializeCliArguments(["--safe-chain-log-file-format=json", "install"]);
assert.strictEqual(getLogFileFormat(), "json");

initializeCliArguments(["install"]);
assert.strictEqual(getLogFileFormat(), undefined);
});

it("should handle log-file and log-file-format together", () => {
const args = [
"--safe-chain-log-file=/tmp/out.log",
"--safe-chain-log-file-format=json",
"install",
];
const result = initializeCliArguments(args);

assert.deepEqual(result, ["install"]);
assert.strictEqual(getLogFile(), "/tmp/out.log");
assert.strictEqual(getLogFileFormat(), "json");
});

it("should not set logFileVerbosity when no log-file-verbosity argument is passed", () => {
initializeCliArguments(["install"]);

assert.strictEqual(getLogFileVerbosity(), undefined);
});

it("should parse log-file-verbosity values and lowercase them", () => {
initializeCliArguments(["--safe-chain-log-file-verbosity=Verbose", "install"]);

assert.strictEqual(getLogFileVerbosity(), "verbose");
});

it("should reset logFileVerbosity between calls", () => {
initializeCliArguments(["--safe-chain-log-file-verbosity=silent"]);
assert.strictEqual(getLogFileVerbosity(), "silent");

initializeCliArguments(["install"]);
assert.strictEqual(getLogFileVerbosity(), undefined);
});

it("should use the last log-file-verbosity argument when multiple are provided", () => {
initializeCliArguments([
"--safe-chain-log-file-verbosity=normal",
"--safe-chain-log-file-verbosity=silent",
"install",
]);

assert.strictEqual(getLogFileVerbosity(), "silent");
});
});
42 changes: 42 additions & 0 deletions packages/safe-chain/src/config/configFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { getSafeChainBaseDir } from "./safeChainDir.js";
* @property {unknown | Number} scanTimeout
* @property {unknown | Number} minimumPackageAgeHours
* @property {unknown | string} malwareListBaseUrl
* @property {unknown | string} logFile
* @property {unknown | string} logFileFormat
* @property {unknown | string} logFileVerbosity
* @property {unknown | SafeChainRegistryConfiguration} npm
* @property {unknown | SafeChainRegistryConfiguration} pip
*
Expand Down Expand Up @@ -98,6 +101,42 @@ export function getMalwareListBaseUrl() {
return undefined;
}

/**
* Gets the log file path from the config file
* @returns {string | undefined}
*/
export function getLogFile() {
const config = readConfigFile();
if (config.logFile && typeof config.logFile === "string") {
return config.logFile;
}
return undefined;
}

/**
* Gets the log file format from the config file
* @returns {string | undefined}
*/
export function getLogFileFormat() {
const config = readConfigFile();
if (config.logFileFormat && typeof config.logFileFormat === "string") {
return config.logFileFormat;
}
return undefined;
}

/**
* Gets the log file verbosity from the config file
* @returns {string | undefined}
*/
export function getLogFileVerbosity() {
const config = readConfigFile();
if (config.logFileVerbosity && typeof config.logFileVerbosity === "string") {
return config.logFileVerbosity;
}
return undefined;
}

/**
* Gets the custom npm registries from the config file (format parsing only, no validation)
* @returns {string[]}
Expand Down Expand Up @@ -229,6 +268,9 @@ function readConfigFile() {
scanTimeout: undefined,
minimumPackageAgeHours: undefined,
malwareListBaseUrl: undefined,
logFile: undefined,
logFileFormat: undefined,
logFileVerbosity: undefined,
npm: {
customRegistries: undefined,
},
Expand Down
Loading
Loading