diff --git a/README.md b/README.md index 1083c0e..7b3f038 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,18 @@ Example usage: npm install suspicious-package --safe-chain-malware-action=prompt ``` +## Logging + +You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag: + +- `--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 +``` + # Usage in CI/CD You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation. diff --git a/packages/safe-chain/src/config/cliArguments.js b/packages/safe-chain/src/config/cliArguments.js index 87abb7b..70c56b1 100644 --- a/packages/safe-chain/src/config/cliArguments.js +++ b/packages/safe-chain/src/config/cliArguments.js @@ -1,5 +1,6 @@ const state = { malwareAction: undefined, + loggingLevel: undefined, }; const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; @@ -7,6 +8,7 @@ const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; export function initializeCliArguments(args) { // Reset state on each call state.malwareAction = undefined; + state.loggingLevel = undefined; const safeChainArgs = []; const remainingArgs = []; @@ -20,6 +22,7 @@ export function initializeCliArguments(args) { } setMalwareAction(safeChainArgs); + setLoggingLevel(safeChainArgs); return remainingArgs; } @@ -48,3 +51,17 @@ function getLastArgEqualsValue(args, prefix) { export function getMalwareAction() { return state.malwareAction; } + +function setLoggingLevel(args) { + const safeChainLoggingArg = SAFE_CHAIN_ARG_PREFIX + "logging="; + + const level = getLastArgEqualsValue(args, safeChainLoggingArg); + if (!level) { + return; + } + state.loggingLevel = level.toLowerCase(); +} + +export function getLoggingLevel() { + return state.loggingLevel; +} diff --git a/packages/safe-chain/src/config/cliArguments.spec.js b/packages/safe-chain/src/config/cliArguments.spec.js index 9d5c0ba..c7d9a84 100644 --- a/packages/safe-chain/src/config/cliArguments.spec.js +++ b/packages/safe-chain/src/config/cliArguments.spec.js @@ -1,6 +1,10 @@ import { describe, it } from "node:test"; import assert from "node:assert"; -import { initializeCliArguments, getMalwareAction } from "./cliArguments.js"; +import { + initializeCliArguments, + getMalwareAction, + getLoggingLevel, +} from "./cliArguments.js"; describe("initializeCliArguments", () => { it("should return all args when no safe-chain args are present", () => { @@ -105,4 +109,67 @@ describe("initializeCliArguments", () => { assert.deepEqual(result, ["install"]); assert.strictEqual(getMalwareAction(), "block"); }); + + it("should not set loggingLevel when no logging argument is passed", () => { + const args = ["install", "express", "--save"]; + initializeCliArguments(args); + + assert.strictEqual(getLoggingLevel(), undefined); + }); + + it("should parse logging=silent and set state", () => { + const args = ["--safe-chain-logging=silent", "install", "package"]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install", "package"]); + assert.strictEqual(getLoggingLevel(), "silent"); + }); + + it("should parse logging=normal and set state", () => { + const args = ["--safe-chain-logging=normal", "install", "package"]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install", "package"]); + assert.strictEqual(getLoggingLevel(), "normal"); + }); + + it("should handle multiple logging args, using the last one", () => { + const args = [ + "--safe-chain-logging=normal", + "--safe-chain-logging=silent", + "install", + ]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install"]); + assert.strictEqual(getLoggingLevel(), "silent"); + }); + + it("should handle logging level case-insensitively", () => { + const args = ["--safe-chain-logging=SILENT", "install"]; + initializeCliArguments(args); + + assert.strictEqual(getLoggingLevel(), "silent"); + }); + + it("should capture invalid logging level as-is (lowercased)", () => { + const args = ["--safe-chain-logging=invalid", "install"]; + initializeCliArguments(args); + + assert.strictEqual(getLoggingLevel(), "invalid"); + }); + + it("should handle logging with other safe-chain args", () => { + const args = [ + "--safe-chain-debug", + "--safe-chain-logging=silent", + "--safe-chain-malware-action=block", + "install", + ]; + const result = initializeCliArguments(args); + + assert.deepEqual(result, ["install"]); + assert.strictEqual(getLoggingLevel(), "silent"); + assert.strictEqual(getMalwareAction(), "block"); + }); }); diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index ed2cae2..17c1cdb 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -10,5 +10,18 @@ export function getMalwareAction() { return MALWARE_ACTION_BLOCK; } +export function getLoggingLevel() { + const level = cliArguments.getLoggingLevel(); + + if (level === LOGGING_SILENT) { + return LOGGING_SILENT; + } + + return LOGGING_NORMAL; +} + export const MALWARE_ACTION_BLOCK = "block"; export const MALWARE_ACTION_PROMPT = "prompt"; + +export const LOGGING_SILENT = "silent"; +export const LOGGING_NORMAL = "normal"; diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 829afa1..99fe90f 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -3,16 +3,27 @@ import chalk from "chalk"; import ora from "ora"; import { createInterface } from "readline"; import { isCi } from "./environment.js"; +import { getLoggingLevel, LOGGING_SILENT } from "../config/settings.js"; + +function isSilentMode() { + return getLoggingLevel() === LOGGING_SILENT; +} function emptyLine() { + if (isSilentMode()) return; + writeInformation(""); } function writeInformation(message, ...optionalParams) { + if (isSilentMode()) return; + console.log(message, ...optionalParams); } function writeWarning(message, ...optionalParams) { + if (isSilentMode()) return; + if (!isCi()) { message = chalk.yellow(message); } @@ -26,7 +37,24 @@ function writeError(message, ...optionalParams) { console.error(message, ...optionalParams); } +function writeExitWithoutInstallingMaliciousPackages() { + let message = "Safe-chain: Exiting without installing malicious packages."; + if (!isCi()) { + message = chalk.red(message); + } + console.error(message); +} + function startProcess(message) { + if (isSilentMode()) { + return { + succeed: () => {}, + fail: () => {}, + stop: () => {}, + setText: () => {}, + }; + } + if (isCi()) { return { succeed: (message) => { @@ -60,7 +88,7 @@ function startProcess(message) { } async function confirm(config) { - if (isCi()) { + if (isCi() || isSilentMode()) { return Promise.resolve(config.default); } @@ -91,6 +119,7 @@ export const ui = { writeInformation, writeWarning, writeError, + writeExitWithoutInstallingMaliciousPackages, emptyLine, startProcess, confirm, diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index b0e8dd1..887fd47 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -153,7 +153,7 @@ function verifyNoMaliciousPackages() { } ui.emptyLine(); - ui.writeError("Exiting without installing malicious packages."); + ui.writeExitWithoutInstallingMaliciousPackages(); ui.emptyLine(); return false; diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index 36f62ca..4b0f654 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -93,7 +93,7 @@ async function onMalwareFound() { } } - ui.writeError("Exiting without installing malicious packages."); + ui.writeExitWithoutInstallingMaliciousPackages(); ui.emptyLine(); return 1; } diff --git a/packages/safe-chain/src/scanning/index.scanCommand.spec.js b/packages/safe-chain/src/scanning/index.scanCommand.spec.js index 1858d10..abcdc97 100644 --- a/packages/safe-chain/src/scanning/index.scanCommand.spec.js +++ b/packages/safe-chain/src/scanning/index.scanCommand.spec.js @@ -46,6 +46,7 @@ describe("scanCommand", async () => { writeError: () => {}, writeInformation: () => {}, writeWarning: () => {}, + writeExitWithoutInstallingMaliciousPackages: () => {}, emptyLine: () => {}, confirm: mockConfirm, },