Merge pull request #123 from AikidoSec/logging-silent-mode

Introduce silent mode to disable logging
This commit is contained in:
Sander Declerck 2025-10-27 11:29:26 +01:00 committed by GitHub
commit 95d9cefcc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 143 additions and 4 deletions

View file

@ -88,6 +88,18 @@ Example usage:
npm install suspicious-package --safe-chain-malware-action=prompt 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 # 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. 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.

View file

@ -1,5 +1,6 @@
const state = { const state = {
malwareAction: undefined, malwareAction: undefined,
loggingLevel: undefined,
}; };
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
@ -7,6 +8,7 @@ const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
export function initializeCliArguments(args) { export function initializeCliArguments(args) {
// Reset state on each call // Reset state on each call
state.malwareAction = undefined; state.malwareAction = undefined;
state.loggingLevel = undefined;
const safeChainArgs = []; const safeChainArgs = [];
const remainingArgs = []; const remainingArgs = [];
@ -20,6 +22,7 @@ export function initializeCliArguments(args) {
} }
setMalwareAction(safeChainArgs); setMalwareAction(safeChainArgs);
setLoggingLevel(safeChainArgs);
return remainingArgs; return remainingArgs;
} }
@ -48,3 +51,17 @@ function getLastArgEqualsValue(args, prefix) {
export function getMalwareAction() { export function getMalwareAction() {
return state.malwareAction; 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;
}

View file

@ -1,6 +1,10 @@
import { describe, it } from "node:test"; import { describe, it } from "node:test";
import assert from "node:assert"; import assert from "node:assert";
import { initializeCliArguments, getMalwareAction } from "./cliArguments.js"; import {
initializeCliArguments,
getMalwareAction,
getLoggingLevel,
} from "./cliArguments.js";
describe("initializeCliArguments", () => { describe("initializeCliArguments", () => {
it("should return all args when no safe-chain args are present", () => { it("should return all args when no safe-chain args are present", () => {
@ -105,4 +109,67 @@ describe("initializeCliArguments", () => {
assert.deepEqual(result, ["install"]); assert.deepEqual(result, ["install"]);
assert.strictEqual(getMalwareAction(), "block"); 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");
});
}); });

View file

@ -10,5 +10,18 @@ export function getMalwareAction() {
return MALWARE_ACTION_BLOCK; 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_BLOCK = "block";
export const MALWARE_ACTION_PROMPT = "prompt"; export const MALWARE_ACTION_PROMPT = "prompt";
export const LOGGING_SILENT = "silent";
export const LOGGING_NORMAL = "normal";

View file

@ -3,16 +3,27 @@ import chalk from "chalk";
import ora from "ora"; import ora from "ora";
import { createInterface } from "readline"; import { createInterface } from "readline";
import { isCi } from "./environment.js"; import { isCi } from "./environment.js";
import { getLoggingLevel, LOGGING_SILENT } from "../config/settings.js";
function isSilentMode() {
return getLoggingLevel() === LOGGING_SILENT;
}
function emptyLine() { function emptyLine() {
if (isSilentMode()) return;
writeInformation(""); writeInformation("");
} }
function writeInformation(message, ...optionalParams) { function writeInformation(message, ...optionalParams) {
if (isSilentMode()) return;
console.log(message, ...optionalParams); console.log(message, ...optionalParams);
} }
function writeWarning(message, ...optionalParams) { function writeWarning(message, ...optionalParams) {
if (isSilentMode()) return;
if (!isCi()) { if (!isCi()) {
message = chalk.yellow(message); message = chalk.yellow(message);
} }
@ -26,7 +37,24 @@ function writeError(message, ...optionalParams) {
console.error(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) { function startProcess(message) {
if (isSilentMode()) {
return {
succeed: () => {},
fail: () => {},
stop: () => {},
setText: () => {},
};
}
if (isCi()) { if (isCi()) {
return { return {
succeed: (message) => { succeed: (message) => {
@ -60,7 +88,7 @@ function startProcess(message) {
} }
async function confirm(config) { async function confirm(config) {
if (isCi()) { if (isCi() || isSilentMode()) {
return Promise.resolve(config.default); return Promise.resolve(config.default);
} }
@ -91,6 +119,7 @@ export const ui = {
writeInformation, writeInformation,
writeWarning, writeWarning,
writeError, writeError,
writeExitWithoutInstallingMaliciousPackages,
emptyLine, emptyLine,
startProcess, startProcess,
confirm, confirm,

View file

@ -153,7 +153,7 @@ function verifyNoMaliciousPackages() {
} }
ui.emptyLine(); ui.emptyLine();
ui.writeError("Exiting without installing malicious packages."); ui.writeExitWithoutInstallingMaliciousPackages();
ui.emptyLine(); ui.emptyLine();
return false; return false;

View file

@ -93,7 +93,7 @@ async function onMalwareFound() {
} }
} }
ui.writeError("Exiting without installing malicious packages."); ui.writeExitWithoutInstallingMaliciousPackages();
ui.emptyLine(); ui.emptyLine();
return 1; return 1;
} }

View file

@ -46,6 +46,7 @@ describe("scanCommand", async () => {
writeError: () => {}, writeError: () => {},
writeInformation: () => {}, writeInformation: () => {},
writeWarning: () => {}, writeWarning: () => {},
writeExitWithoutInstallingMaliciousPackages: () => {},
emptyLine: () => {}, emptyLine: () => {},
confirm: mockConfirm, confirm: mockConfirm,
}, },