Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
9d1f7ac
Use ramaproxy if it's available.
SanderDeclerck Jan 12, 2026
f6abfb8
Install script for linux x64 as well
SanderDeclerck Jan 12, 2026
22580bc
Set correct version
SanderDeclerck Jan 12, 2026
6006760
Only inherit io when loglevel verbose
SanderDeclerck Jan 12, 2026
0411a57
Wait and poll until proxy starts for max 60s
SanderDeclerck Jan 13, 2026
67a8f2d
Use prroxy version 0.0.4
SanderDeclerck Jan 13, 2026
83b62f9
v0.0.5-linux-proxy-bins of proxy
SanderDeclerck Jan 13, 2026
c37b0c8
Proxy version v0.0.7-linux-proxy-bins
SanderDeclerck Jan 15, 2026
6fcca09
Add link to linux-arm64 proxy
SanderDeclerck Jan 15, 2026
2cba4be
Include package name in logging when minimum package age is not met
reiniercriel Jan 13, 2026
a7388bb
Update packages/safe-chain/src/registryProxy/interceptors/npm/modifyN…
bitterpanda63 Jan 13, 2026
7377b55
Add Bitbucket Pipelines example
slootjes Jan 13, 2026
14e94dc
Retry downloading the malware database 3 times
SanderDeclerck Jan 14, 2026
4a53a7b
Add tests for malware db retry
SanderDeclerck Jan 14, 2026
cf8e39c
Handle pr comments
SanderDeclerck Jan 14, 2026
0e6d002
Don't swallow error on retry
SanderDeclerck Jan 14, 2026
3210b68
Update packages/safe-chain/src/api/aikido.js
bitterpanda63 Jan 14, 2026
b7f793f
Attempted fix for powershell swallowing '--'
reiniercriel Jan 12, 2026
5c43129
Fix some logic
reiniercriel Jan 12, 2026
4ef4218
Remove comment
reiniercriel Jan 12, 2026
d7a9884
Allow to exclude packages from the minimum package age
SanderDeclerck Jan 14, 2026
2d60906
Allow trailing * for wildcard matching
SanderDeclerck Jan 14, 2026
20cc62d
Only allow wildcards for scoped packages (@scope/*)
SanderDeclerck Jan 15, 2026
607b4ee
Propagate command-not-found errors when invoking wrapped commands
uriel-ecosia Jan 7, 2026
11d9e26
init-posix: preserve arguments when exec'ing the original_cmd
uriel-ecosia Jan 8, 2026
b1fa9f5
Add the same handler for fish
uriel-ecosia Jan 8, 2026
dba101d
Add ultimate installer for Windows
SanderDeclerck Jan 19, 2026
2a649c5
Start and stop safe-chain agent's Windows service.
SanderDeclerck Jan 19, 2026
7f6ce79
Overwrite the agent if it's already installed.
SanderDeclerck Jan 19, 2026
8410b94
Improve updating existing agent install
SanderDeclerck Jan 19, 2026
2bfce02
Fix linting
SanderDeclerck Jan 19, 2026
14ff245
Uninstall safe-chain agent if it's there, before re-installing
SanderDeclerck Jan 19, 2026
0be42c8
Parse cli args in ultimate installation
SanderDeclerck Jan 19, 2026
bee196c
Check if the agents service is running before starting it
SanderDeclerck Jan 19, 2026
d03a3a3
Improve output
SanderDeclerck Jan 19, 2026
27980ae
Restructure code into separate files
SanderDeclerck Jan 19, 2026
fa94784
Move download name construction to os installer function
SanderDeclerck Jan 19, 2026
d86246a
Handle code quality comments
SanderDeclerck Jan 19, 2026
67b4be8
Log when installer file cleanup failed
SanderDeclerck Jan 19, 2026
5fd3ce0
Use safeSpawn instead of execSync
SanderDeclerck Jan 19, 2026
e9b1c48
Code quality: use early return
SanderDeclerck Jan 19, 2026
3483219
Improve error handling
SanderDeclerck Jan 19, 2026
3c40c60
Write error output
SanderDeclerck Jan 19, 2026
3888881
Temporarily disable cleanup
SanderDeclerck Jan 19, 2026
a7315d2
Write stdout stderr
SanderDeclerck Jan 19, 2026
1de6a4a
Use execSync to execute powershell command
SanderDeclerck Jan 19, 2026
fc43d93
Fix uninstall
SanderDeclerck Jan 19, 2026
641bfe9
Cleanup debug logging
SanderDeclerck Jan 19, 2026
09130c3
Add explaining comments for powershell scritps
SanderDeclerck Jan 19, 2026
07aa10d
Fix naming of SafeChain Agent
bitterpanda63 Jan 19, 2026
e7de25d
Update packages/safe-chain/src/installation/installOnWindows.js
bitterpanda63 Jan 19, 2026
a1d6f31
Move os and arch detection to downloader, add checksum verification.
SanderDeclerck Jan 20, 2026
457b71a
Don't start the windows service - the msi already does this
SanderDeclerck Jan 20, 2026
6c1383a
Update download urls
SanderDeclerck Jan 21, 2026
c8ee15d
Add mac os installation
SanderDeclerck Jan 21, 2026
a4e9036
Remove unused variable
SanderDeclerck Jan 21, 2026
92ec4e4
PR comments
SanderDeclerck Jan 22, 2026
b3a0ac8
Fix linting
SanderDeclerck Jan 22, 2026
b004894
Fix download links
SanderDeclerck Jan 22, 2026
f283b72
Update application names on Windows
SanderDeclerck Jan 22, 2026
cf57a98
Support Windows in install-safe-shain.sh (git bash, cygwin, ...)
SanderDeclerck Jan 20, 2026
16e6d0a
Fix tests for mitm registryproxy
SanderDeclerck Jan 22, 2026
d8b703e
Add message about the certificate popup
SanderDeclerck Jan 22, 2026
dcebe73
Don't insert empty line in rc file when it already ends with an empty…
SanderDeclerck Jan 27, 2026
53b7d92
Add uninstallation process for ultimate
SanderDeclerck Jan 27, 2026
ad18829
Bump safe-chain-internals version
SanderDeclerck Jan 27, 2026
8b8c294
Verify download links in a test
SanderDeclerck Jan 27, 2026
c3282e3
Remove duplicate "Stopping running service" log
SanderDeclerck Jan 27, 2026
364061b
Update commands for ultimate
SanderDeclerck Jan 27, 2026
8aad04e
PR comment: extract requireRootPrivileges / requireAdminPrivileges in…
SanderDeclerck Jan 28, 2026
81bf46d
Rename output
SanderDeclerck Jan 28, 2026
2073140
Mac: use uninstaller script
SanderDeclerck Jan 28, 2026
8ca361e
Fix uninstall script
SanderDeclerck Jan 28, 2026
1c3037a
Bump agent version to v1.0.0
SanderDeclerck Jan 29, 2026
162ac2c
Add troubleshooting steps for powershell when executionpolicy doens't…
SanderDeclerck Jan 30, 2026
3042a9e
add safe-chain ultimate logs
bitterpanda63 Jan 30, 2026
a904e9c
install archiver
bitterpanda63 Jan 30, 2026
3bec78a
create an export async collectLogs
bitterpanda63 Jan 30, 2026
dbfff46
add docs & collect-logs to safe-chain bin
bitterpanda63 Jan 30, 2026
ff924c0
use path.resolve to print full file
bitterpanda63 Jan 30, 2026
0752139
add 'archiver' types
bitterpanda63 Jan 30, 2026
1adb2b3
Cleanup linting errors
bitterpanda63 Jan 30, 2026
4079bff
rename to troubleshooting-*
bitterpanda63 Jan 30, 2026
8cd3c8a
troubleshooting-export: update description
bitterpanda63 Jan 30, 2026
4ab4633
Revert "add 'archiver' types"
bitterpanda63 Jan 30, 2026
ccef402
Revert "install archiver"
bitterpanda63 Jan 30, 2026
78e4a43
install deps in safe-chain/package.json
bitterpanda63 Jan 30, 2026
e01b06b
Verify the number of arguments for ultimate commands
SanderDeclerck Feb 2, 2026
aa667d4
Add rama proxy feature flag
SanderDeclerck Feb 11, 2026
6cd1115
Update proxy version to 1.0.0
SanderDeclerck Feb 11, 2026
03ecd0d
Merge branch 'main' into new-proxy-beta
SanderDeclerck Feb 11, 2026
ca07172
Move existing proxy files to builtInProxy folder
SanderDeclerck Feb 11, 2026
7170f9a
Fix typecheck
SanderDeclerck Feb 11, 2026
9a7c054
Merge branch 'main' into new-proxy-beta
SanderDeclerck Feb 27, 2026
ba604ea
Use CA bundle when using rama proxy
SanderDeclerck Feb 12, 2026
0fdfbf6
Fix readme duplication
SanderDeclerck Feb 27, 2026
912f62f
Merge branch 'main' into new-proxy-beta
SanderDeclerck Mar 2, 2026
e8a4fbc
Cleanup formatting changes from PR
SanderDeclerck Mar 2, 2026
b03c1f6
Cleanup pt2
SanderDeclerck Mar 2, 2026
68352d9
Use proxy reporting endpoint to subscribe to blocked events
SanderDeclerck Mar 3, 2026
8c38c0e
Add tests
SanderDeclerck Mar 3, 2026
4822119
Cleanup reportingServer code
SanderDeclerck Mar 3, 2026
f480d22
Add missing await
SanderDeclerck Mar 9, 2026
261aca9
Clean up rama proxy process start
SanderDeclerck Mar 9, 2026
8086f6e
Fix verbose logging
SanderDeclerck Mar 9, 2026
cf63134
Merge pull request #286 from AikidoSec/new-proxy-beta
SanderDeclerck Mar 9, 2026
e3fc665
Merge branch 'rama-integration-beta' into rama-blocked-events
SanderDeclerck Mar 9, 2026
127447d
Code quality comments
SanderDeclerck Mar 9, 2026
ceefaab
Consume the safe chain proxy min package age reporting webhook
SanderDeclerck Mar 10, 2026
1b32be6
Fix tests
SanderDeclerck Mar 10, 2026
983f26e
Adapt to modified contract with rama proxy
SanderDeclerck Mar 11, 2026
c5d6413
Merge pull request #328 from AikidoSec/rama-blocked-events
bitterpanda63 Apr 28, 2026
64a825f
Merge branch 'main' into rama-integration-beta
SanderDeclerck May 4, 2026
9f0e1ae
Merge branch 'main' into rama-integration-beta
SanderDeclerck May 4, 2026
5f82e45
Merge branch 'rama-integration-beta' into rama-min-package-age-reporting
SanderDeclerck May 4, 2026
c7ec7fc
Fix linting and type errors
SanderDeclerck May 4, 2026
dc1bbea
Undo merge issue
SanderDeclerck May 5, 2026
2412779
Add package name again
SanderDeclerck May 5, 2026
6442c4c
Fix linting
SanderDeclerck May 5, 2026
f2479ad
Listen to blocks with reason new_package
SanderDeclerck May 5, 2026
62a3c13
Expose minPackageAgeVersionsSuppressed for pypi as well.
SanderDeclerck May 5, 2026
fbc94f7
Remove suppressedVersionState.js as it's no longer used
SanderDeclerck May 5, 2026
eedbac7
Merge pull request #332 from AikidoSec/rama-min-package-age-reporting
bitterpanda63 May 6, 2026
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
31 changes: 31 additions & 0 deletions install-scripts/install-safe-chain.sh
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,9 @@ parse_arguments() {
--include-python)
warn "--include-python is deprecated and ignored. Python ecosystem is now included by default."
;;
--experimental-rama-proxy)
USE_RAMA_PROXY=true
;;
*)
error "Unknown argument: $1"
;;
Expand All @@ -444,6 +447,7 @@ parse_arguments() {
main() {
# Initialize argument flags
USE_CI_SETUP=false
USE_RAMA_PROXY=false

# Parse command-line arguments
parse_arguments "$@"
Expand Down Expand Up @@ -503,6 +507,33 @@ main() {

info "Binary installed to: $FINAL_FILE"

# Download safechain-proxy
if [ "$USE_RAMA_PROXY" = "true" ] && { [ "$OS" = "macos" ] || [ "$OS" = "linux" ] || [ "$OS" = "linuxstatic" ]; }; then
info "Downloading safechain-proxy..."

if [ "$OS" = "macos" ]; then
if [ "$ARCH" = "arm64" ]; then
PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.0.0/safechain-proxy-darwin-arm64"
else
PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.0.0/safechain-proxy-darwin-amd64"
fi
else
# Linux (both linux and linuxstatic)
if [ "$ARCH" = "x64" ]; then
PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.0.0/safechain-proxy-linux-amd64"
else
PROXY_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.0.0/safechain-proxy-linux-arm64"
fi
fi

if [ -n "$PROXY_URL" ]; then
PROXY_FILE="${INSTALL_DIR}/safechain-proxy"
download "$PROXY_URL" "$PROXY_FILE"
chmod +x "$PROXY_FILE" || error "Failed to make proxy executable"
info "Proxy installed to: $PROXY_FILE"
fi
fi

run_setup_command "$FINAL_FILE"
}

Expand Down
107 changes: 94 additions & 13 deletions packages/safe-chain/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,22 @@ export async function main(args) {
process.on("SIGINT", handleProcessTermination);
process.on("SIGTERM", handleProcessTermination);

/** @type {import("./registryProxy/registryProxy.js").PackageBlockedEvent[]} */
let malwareBlockedEvents = [];
/** @type {import("./registryProxy/registryProxy.js").PackageBlockedEvent[]} */
let minPackageAgeBlocks = [];
/** @type {import("./registryProxy/registryProxy.js").MinPackageAgeSuppressionEvent[]} */
let suppressedVersionEvents = [];

const proxy = createSafeChainProxy();
await proxy.startServer();
proxy.addListener("malwareBlocked", (ev) => malwareBlockedEvents.push(ev));
proxy.addListener("minPackageAgeVersionsSuppressed", (ev) =>
suppressedVersionEvents.push(ev),
);
proxy.addListener("minimumAgeRequestBlocked", (ev) =>
minPackageAgeBlocks.push(ev),
);

// Global error handlers to log unhandled errors
process.on("uncaughtException", (error) => {
Expand Down Expand Up @@ -64,11 +78,13 @@ export async function main(args) {
// Write all buffered logs
ui.writeBufferedLogsAndStopBuffering();

if (proxy.hasBlockedMaliciousPackages()) {
if (malwareBlockedEvents.length > 0) {
printBlockedMalware(malwareBlockedEvents);
return 1;
}

if (proxy.hasBlockedMinimumAgeRequests()) {
if (minPackageAgeBlocks.length > 0) {
printMinPackageAgeBlocks(minPackageAgeBlocks);
return 1;
}

Expand All @@ -81,17 +97,8 @@ export async function main(args) {
);
}

if (proxy.hasSuppressedVersions()) {
ui.writeInformation(
`${chalk.yellow(
"ℹ",
)} Safe-chain: Some package versions were suppressed during package metadata resolution due to minimum package age.`,
);
ui.writeInformation(
` To disable this check, use: ${chalk.cyan(
"--safe-chain-skip-minimum-package-age",
)}`,
);
if (suppressedVersionEvents.length > 0) {
printSuppressedVersions(suppressedVersionEvents);
}

// Returning the exit code back to the caller allows the promise
Expand Down Expand Up @@ -121,3 +128,77 @@ function isSafeChainVerify(args) {
return true;
}
}

/**
*
* @param {import("./registryProxy/registryProxy.js").PackageBlockedEvent[]} malwareBlockedEvents
*/
function printBlockedMalware(malwareBlockedEvents) {
ui.emptyLine();

ui.writeInformation(
`Safe-chain: ${chalk.bold(
`blocked ${malwareBlockedEvents.length} malicious package downloads`,
)}:`,
);

for (const ev of malwareBlockedEvents) {
ui.writeInformation(` - ${ev.packageName}@${ev.packageVersion}`);
}

ui.emptyLine();
ui.writeExitWithoutInstallingMaliciousPackages();
ui.emptyLine();
}

/**
*
* @param {import("./registryProxy/registryProxy.js").PackageBlockedEvent[]} minPackageAgeBlocks
*/
function printMinPackageAgeBlocks(minPackageAgeBlocks) {
ui.emptyLine();

ui.writeInformation(
`Safe-chain: ${chalk.bold(
`blocked ${minPackageAgeBlocks.length} direct package download request(s) due to minimum package age`,
)}:`,
);

for (const req of minPackageAgeBlocks) {
ui.writeInformation(` - ${req.packageName}@${req.packageVersion}`);
}

ui.writeInformation(
` To disable this check, use: ${chalk.cyan(
"--safe-chain-skip-minimum-package-age",
)}`,
);

ui.emptyLine();
ui.writeError(
"Safe-chain: Exiting without installing packages blocked by the direct download minimum package age check.",
);
ui.emptyLine();
}

/**
*
* @param {import("./registryProxy/registryProxy.js").MinPackageAgeSuppressionEvent[]} minPackageAgeSuppressionEvents
*/
function printSuppressedVersions(minPackageAgeSuppressionEvents) {
ui.writeVerbose(
`${chalk.yellow(
"ℹ",
)} Safe-chain: Some package versions were suppressed during package metadata resolution due to minimum package age:`,
);

for (const ev of minPackageAgeSuppressionEvents) {
ui.writeVerbose(` - ${ev.packageName} (${ev.packageVersions.join(", ")})`);
}

ui.writeVerbose(
` To disable this check, use: ${chalk.cyan(
"--safe-chain-skip-minimum-package-age",
)}`,
);
}
8 changes: 5 additions & 3 deletions packages/safe-chain/src/packagemanager/pip/runPipCommand.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
import {
getProxySettings,
mergeSafeChainProxyEnvironmentVariables,
} from "../../registryProxy/registryProxy.js";
import { PIP_COMMAND, PIP3_COMMAND, PYTHON_COMMAND, PYTHON3_COMMAND } from "./pipSettings.js";
import fs from "node:fs/promises";
import fsSync from "node:fs";
Expand Down Expand Up @@ -100,7 +102,7 @@ export async function runPip(command, args) {
// Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots + user certs)
// so that any network request made by pip, including those outside explicit CLI args,
// validates correctly under both MITM'd and tunneled HTTPS.
const combinedCaPath = getCombinedCaBundlePath();
const combinedCaPath = getProxySettings().caCertBundlePath;

// Commands that need access to persistent config/cache/state files
// These should not have PIP_CONFIG_FILE overridden as it would prevent them from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ describe("runPipCommand environment variable handling", () => {
HTTPS_PROXY: "http://localhost:8080",
HTTP_PROXY: "",
}),
getProxySettings: () => {
return {
proxyUrl: "http://localhost:8080",
caCertBundlePath: "/tmp/test-combined-ca.pem",
};
},
},
});

Expand Down
8 changes: 5 additions & 3 deletions packages/safe-chain/src/packagemanager/pipx/runPipXCommand.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
import {
getProxySettings,
mergeSafeChainProxyEnvironmentVariables,
} from "../../registryProxy/registryProxy.js";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";

/**
Expand Down Expand Up @@ -42,7 +44,7 @@ export async function runPipX(command, args) {
try {
const env = mergeSafeChainProxyEnvironmentVariables(process.env);

const combinedCaPath = getCombinedCaBundlePath();
const combinedCaPath = getProxySettings().caCertBundlePath;
const modifiedEnv = getPipXCaBundleEnvironmentVariables(env, combinedCaPath);

// Note: pipx uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ describe("runPipXCommand", () => {
mergeCalls.push(env);
return { ...env, ...mergedEnvReturn };
},
getProxySettings: () => {
return {
proxyUrl: "",
caCertBundlePath: "/tmp/test-combined-ca.pem",
};
},
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
import {
getProxySettings,
mergeSafeChainProxyEnvironmentVariables,
} from "../../registryProxy/registryProxy.js";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";

/**
Expand Down Expand Up @@ -57,7 +59,7 @@ async function runPoetryCommand(args) {
try {
const env = mergeSafeChainProxyEnvironmentVariables(process.env);

const combinedCaPath = getCombinedCaBundlePath();
const combinedCaPath = getProxySettings().caCertBundlePath;
setPoetryCaBundleEnvironmentVariables(env, combinedCaPath);

const result = await safeSpawn("poetry", args, {
Expand Down
8 changes: 5 additions & 3 deletions packages/safe-chain/src/packagemanager/uv/runUvCommand.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
import {
getProxySettings,
mergeSafeChainProxyEnvironmentVariables,
} from "../../registryProxy/registryProxy.js";
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";

/**
Expand Down Expand Up @@ -48,7 +50,7 @@ export async function runUv(command, args) {
try {
const env = mergeSafeChainProxyEnvironmentVariables(process.env);

const combinedCaPath = getCombinedCaBundlePath();
const combinedCaPath = getProxySettings().caCertBundlePath;
setUvCaBundleEnvironmentVariables(env, combinedCaPath);

// Note: uv uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import forge from "node-forge";
import path from "path";
import fs from "fs";
import { getCertsDir } from "../config/safeChainDir.js";
import { getCertsDir } from "../../config/safeChainDir.js";

const ca = loadCa();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe("certUtils", () => {

beforeEach(() => {
installedSafeChainDir = undefined;
mock.module("../config/safeChainDir.js", {
mock.module("../../config/safeChainDir.js", {
namedExports: {
getSafeChainBaseDir: () => installedSafeChainDir ?? "/home/test/.safe-chain",
getCertsDir: () => `${installedSafeChainDir ?? "/home/test/.safe-chain"}/certs`,
Expand Down
Loading
Loading