Merge pull request #128 from AikidoSec/verbose-logging

This commit is contained in:
bitterpanda 2025-11-04 11:01:12 +01:00 committed by GitHub
commit 1f208d8784
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 115 additions and 14 deletions

View file

@ -88,6 +88,14 @@ Example usage:
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
```
# 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.

View file

@ -9,7 +9,8 @@
"scripts": {
"test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun",
"test:e2e": "npm run test --workspace=test/e2e",
"lint": "npm run lint --workspace=packages/safe-chain"
"lint": "npm run lint --workspace=packages/safe-chain",
"typecheck": "npm run typecheck --workspace=packages/safe-chain"
},
"repository": {
"type": "git",

View file

@ -7,8 +7,13 @@ export function getLoggingLevel() {
return LOGGING_SILENT;
}
if (level === LOGGING_VERBOSE) {
return LOGGING_VERBOSE;
}
return LOGGING_NORMAL;
}
export const LOGGING_SILENT = "silent";
export const LOGGING_NORMAL = "normal";
export const LOGGING_VERBOSE = "verbose";

View file

@ -2,12 +2,28 @@
import chalk from "chalk";
import ora from "ora";
import { isCi } from "./environment.js";
import { getLoggingLevel, LOGGING_SILENT } from "../config/settings.js";
import {
getLoggingLevel,
LOGGING_SILENT,
LOGGING_VERBOSE,
} from "../config/settings.js";
/**
* @type {{ bufferOutput: boolean, bufferedMessages:(() => void)[]}}
*/
const state = {
bufferOutput: false,
bufferedMessages: [],
};
function isSilentMode() {
return getLoggingLevel() === LOGGING_SILENT;
}
function isVerboseMode() {
return getLoggingLevel() === LOGGING_VERBOSE;
}
function emptyLine() {
if (isSilentMode()) return;
@ -22,7 +38,7 @@ function emptyLine() {
function writeInformation(message, ...optionalParams) {
if (isSilentMode()) return;
console.log(message, ...optionalParams);
writeOrBuffer(() => console.log(message, ...optionalParams));
}
/**
@ -36,7 +52,7 @@ function writeWarning(message, ...optionalParams) {
if (!isCi()) {
message = chalk.yellow(message);
}
console.warn(message, ...optionalParams);
writeOrBuffer(() => console.warn(message, ...optionalParams));
}
/**
@ -48,7 +64,7 @@ function writeError(message, ...optionalParams) {
if (!isCi()) {
message = chalk.red(message);
}
console.error(message, ...optionalParams);
writeOrBuffer(() => console.error(message, ...optionalParams));
}
function writeExitWithoutInstallingMaliciousPackages() {
@ -56,7 +72,30 @@ function writeExitWithoutInstallingMaliciousPackages() {
if (!isCi()) {
message = chalk.red(message);
}
console.error(message);
writeOrBuffer(() => console.error(message));
}
/**
* @param {string} message
* @param {...any} optionalParams
* @returns {void}
*/
function writeVerbose(message, ...optionalParams) {
if (!isVerboseMode()) return;
writeOrBuffer(() => console.log(message, ...optionalParams));
}
/**
*
* @param {() => void} messageFunction
*/
function writeOrBuffer(messageFunction) {
if (state.bufferOutput) {
state.bufferedMessages.push(messageFunction);
} else {
messageFunction();
}
}
/**
@ -114,11 +153,27 @@ function startProcess(message) {
}
}
function startBufferingLogs() {
state.bufferOutput = true;
state.bufferedMessages = [];
}
function writeBufferedLogsAndStopBuffering() {
state.bufferOutput = false;
for (const log of state.bufferedMessages) {
log();
}
state.bufferedMessages = [];
}
export const ui = {
writeVerbose,
writeInformation,
writeWarning,
writeError,
writeExitWithoutInstallingMaliciousPackages,
emptyLine,
startProcess,
startBufferingLogs,
writeBufferedLogsAndStopBuffering,
};

View file

@ -12,13 +12,15 @@ import chalk from "chalk";
* @returns {Promise<number | never[]>}
*/
export async function main(args) {
process.on("SIGINT", handleProcessTermination);
process.on("SIGTERM", handleProcessTermination);
const proxy = createSafeChainProxy();
await proxy.startServer();
// Global error handlers to log unhandled errors
process.on("uncaughtException", (error) => {
ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`);
// @ts-expect-error writeVerbose will be added in a future PR
ui.writeVerbose(`Stack trace: ${error.stack}`);
process.exit(1);
});
@ -26,7 +28,6 @@ export async function main(args) {
process.on("unhandledRejection", (reason) => {
ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`);
if (reason instanceof Error) {
// @ts-expect-error writeVerbose will be added in a future PR
ui.writeVerbose(`Stack trace: ${reason.stack}`);
}
process.exit(1);
@ -46,8 +47,16 @@ export async function main(args) {
}
}
// Buffer logs during package manager execution, this avoids interleaving
// of logs from the package manager and safe-chain
// Not doing this could cause bugs to disappear when cursor movement codes
// are written by the package manager while safe-chain is writing logs
ui.startBufferingLogs();
const packageManagerResult = await getPackageManager().runCommand(args);
// Write all buffered logs
ui.writeBufferedLogsAndStopBuffering();
if (!proxy.verifyNoMaliciousPackages()) {
return 1;
}
@ -72,3 +81,7 @@ export async function main(args) {
await proxy.stopServer();
}
}
function handleProcessTermination() {
ui.writeBufferedLogsAndStopBuffering();
}

View file

@ -9,9 +9,13 @@ import { ui } from "../environment/userInteraction.js";
* @param {(target: string) => Promise<boolean>} isAllowed
*/
export function mitmConnect(req, clientSocket, isAllowed) {
ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`);
const { hostname } = new URL(`http://${req.url}`);
clientSocket.on("error", () => {
clientSocket.on("error", (err) => {
ui.writeVerbose(
`Safe-chain: Client socket error for ${req.url}: ${err.message}`
);
// NO-OP
// This can happen if the client TCP socket sends RST instead of FIN.
// Not subscribing to 'close' event will cause node to throw and crash.
@ -56,6 +60,7 @@ function createHttpsServer(hostname, isAllowed) {
const targetUrl = `https://${hostname}${pathAndQuery}`;
if (!(await isAllowed(targetUrl))) {
ui.writeVerbose(`Safe-chain: Blocking request to ${targetUrl}`);
res.writeHead(403, "Forbidden - blocked by safe-chain");
res.end("Blocked by safe-chain");
return;
@ -96,7 +101,10 @@ function getRequestPathAndQuery(url) {
function forwardRequest(req, hostname, res) {
const proxyReq = createProxyRequest(hostname, req, res);
proxyReq.on("error", () => {
proxyReq.on("error", (err) => {
ui.writeVerbose(
`Safe-chain: Error occurred while proxying request: ${err.message}`
);
res.writeHead(502);
res.end("Bad Gateway");
});
@ -111,6 +119,9 @@ function forwardRequest(req, hostname, res) {
});
req.on("end", () => {
ui.writeVerbose(
`Safe-chain: Finished proxying request to ${req.url} for ${hostname}`
);
proxyReq.end();
});
}

View file

@ -138,6 +138,7 @@ function handleConnect(req, clientSocket, head) {
mitmConnect(req, clientSocket, isAllowedUrl);
} else {
// For other hosts, just tunnel the request to the destination tcp socket
ui.writeVerbose(`Safe-chain: Tunneling request to ${req.url}`);
tunnelRequest(req, clientSocket, head);
}
}

View file

@ -1,3 +1,4 @@
import { ui } from "../../environment/userInteraction.js";
import {
MALWARE_STATUS_MALWARE,
openMalwareDatabase,
@ -38,8 +39,14 @@ export async function auditChanges(changes) {
);
if (malwarePackage) {
ui.writeVerbose(
`Safe-chain: Package ${change.name}@${change.version} is marked as malware: ${malwarePackage.status}`
);
disallowedChanges.push({ ...change, reason: malwarePackage.status });
} else {
ui.writeVerbose(
`Safe-chain: Package ${change.name}@${change.version} is clean`
);
allowedChanges.push(change);
}
}