@aikidosec/safe-chain 1.3.3 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,8 +33,6 @@ Aikido Safe Chain supports the following package managers:
33
33
 
34
34
  Installing the Aikido Safe Chain is easy with our one-line installer.
35
35
 
36
- > ⚠️ **Already installed via npm?** See the [migration guide](https://github.com/AikidoSec/safe-chain/blob/main/docs/npm-to-binary-migration.md) to switch to the binary version.
37
-
38
36
  ### Unix/Linux/macOS
39
37
 
40
38
  ```shell
@@ -71,7 +69,20 @@ You can find all available versions on the [releases page](https://github.com/Ai
71
69
 
72
70
  - This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, pip, pip3, poetry, uv and pipx are loaded correctly. If you do not restart your terminal, the aliases will not be available.
73
71
 
74
- 2. **Verify the installation** by running one of the following commands:
72
+ 2. **Verify the installation** by running the verification command:
73
+
74
+ ```shell
75
+ npm safe-chain-verify
76
+ pnpm safe-chain-verify
77
+ pip safe-chain-verify
78
+ uv safe-chain-verify
79
+
80
+ # Any other supported package manager: {packagemanager} safe-chain-verify
81
+ ```
82
+
83
+ - The output should display "OK: Safe-chain works!" confirming that Aikido Safe Chain is properly installed and running.
84
+
85
+ 3. **(Optional) Test malware blocking** by attempting to install a test package:
75
86
 
76
87
  For JavaScript/Node.js:
77
88
 
@@ -188,9 +199,14 @@ You can set the minimum package age through multiple sources (in order of priori
188
199
  }
189
200
  ```
190
201
 
191
- ## Custom NPM Registries
202
+ ## Custom Registries
203
+
204
+ Configure Safe Chain to scan packages from custom or private registries.
205
+
206
+ Supported ecosystems:
192
207
 
193
- Configure Safe Chain to scan packages from custom or private npm registries.
208
+ - Node.js
209
+ - Python
194
210
 
195
211
  ### Configuration Options
196
212
 
@@ -200,6 +216,7 @@ You can set custom registries through environment variable or config file. Both
200
216
 
201
217
  ```shell
202
218
  export SAFE_CHAIN_NPM_CUSTOM_REGISTRIES="npm.company.com,registry.internal.net"
219
+ export SAFE_CHAIN_PIP_CUSTOM_REGISTRIES="pip.company.com,registry.internal.net"
203
220
  ```
204
221
 
205
222
  2. **Config File** (`~/.aikido/config.json`):
@@ -208,6 +225,9 @@ You can set custom registries through environment variable or config file. Both
208
225
  {
209
226
  "npm": {
210
227
  "customRegistries": ["npm.company.com", "registry.internal.net"]
228
+ },
229
+ "pip": {
230
+ "customRegistries": ["pip.company.com", "registry.internal.net"]
211
231
  }
212
232
  }
213
233
  ```
@@ -237,6 +257,7 @@ iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download
237
257
  - ✅ **GitHub Actions**
238
258
  - ✅ **Azure Pipelines**
239
259
  - ✅ **CircleCI**
260
+ - ✅ **Jenkins**
240
261
 
241
262
  ## GitHub Actions Example
242
263
 
@@ -288,4 +309,42 @@ workflows:
288
309
  - build
289
310
  ```
290
311
 
312
+ ## Jenkins Example
313
+
314
+ Note: This assumes Node.js and npm are installed on the Jenkins agent.
315
+
316
+ ```groovy
317
+ pipeline {
318
+ agent any
319
+
320
+ environment {
321
+ // Jenkins does not automatically persist PATH updates from setup-ci,
322
+ // so add the shims + binary directory explicitly for all stages.
323
+ PATH = "${env.HOME}/.safe-chain/shims:${env.HOME}/.safe-chain/bin:${env.PATH}"
324
+ }
325
+
326
+ stages {
327
+ stage('Install safe-chain') {
328
+ steps {
329
+ sh '''
330
+ set -euo pipefail
331
+
332
+ # Install Safe Chain for CI
333
+ curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
334
+ '''
335
+ }
336
+ }
337
+
338
+ stage('Install project dependencies etc...') {
339
+ steps {
340
+ sh '''
341
+ set -euo pipefail
342
+ npm ci
343
+ '''
344
+ }
345
+ }
346
+ }
347
+ }
348
+ ```
349
+
291
350
  After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.
package/bin/safe-chain.js CHANGED
@@ -3,7 +3,10 @@
3
3
  import chalk from "chalk";
4
4
  import { ui } from "../src/environment/userInteraction.js";
5
5
  import { setup } from "../src/shell-integration/setup.js";
6
- import { teardown, teardownDirectories } from "../src/shell-integration/teardown.js";
6
+ import {
7
+ teardown,
8
+ teardownDirectories,
9
+ } from "../src/shell-integration/teardown.js";
7
10
  import { setupCi } from "../src/shell-integration/setup-ci.js";
8
11
  import { initializeCliArguments } from "../src/config/cliArguments.js";
9
12
  import { setEcoSystem } from "../src/config/settings.js";
@@ -45,7 +48,7 @@ if (tool) {
45
48
  const args = process.argv.slice(3);
46
49
 
47
50
  setEcoSystem(tool.ecoSystem);
48
-
51
+
49
52
  // Provide tool context to PM (pip uses this; others ignore)
50
53
  const toolContext = { tool: tool.tool, args };
51
54
  initializePackageManager(tool.internalPackageManagerName, toolContext);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "scripts": {
5
5
  "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
6
6
  "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
@@ -11,6 +11,7 @@ import { getEcoSystem } from "./settings.js";
11
11
  * @property {unknown | Number} scanTimeout
12
12
  * @property {unknown | Number} minimumPackageAgeHours
13
13
  * @property {unknown | SafeChainRegistryConfiguration} npm
14
+ * @property {unknown | SafeChainRegistryConfiguration} pip
14
15
  *
15
16
  * @typedef {Object} SafeChainRegistryConfiguration
16
17
  * We cannot trust the input and should add the necessary validations.
@@ -104,6 +105,28 @@ export function getNpmCustomRegistries() {
104
105
  return customRegistries.filter((item) => typeof item === "string");
105
106
  }
106
107
 
108
+ /**
109
+ * Gets the custom npm registries from the config file (format parsing only, no validation)
110
+ * @returns {string[]}
111
+ */
112
+ export function getPipCustomRegistries() {
113
+ const config = readConfigFile();
114
+
115
+ if (!config || !config.pip) {
116
+ return [];
117
+ }
118
+
119
+ // TypeScript needs help understanding that config.pip exists and has customRegistries
120
+ const pipConfig = /** @type {SafeChainRegistryConfiguration} */ (config.pip);
121
+ const customRegistries = pipConfig.customRegistries;
122
+
123
+ if (!Array.isArray(customRegistries)) {
124
+ return [];
125
+ }
126
+
127
+ return customRegistries.filter((item) => typeof item === "string");
128
+ }
129
+
107
130
  /**
108
131
  * @param {import("../api/aikido.js").MalwarePackage[]} data
109
132
  * @param {string | number} version
@@ -169,6 +192,9 @@ function readConfigFile() {
169
192
  npm: {
170
193
  customRegistries: undefined,
171
194
  },
195
+ pip: {
196
+ customRegistries: undefined,
197
+ },
172
198
  };
173
199
 
174
200
  const configFilePath = getConfigFilePath();
@@ -15,3 +15,13 @@ export function getMinimumPackageAgeHours() {
15
15
  export function getNpmCustomRegistries() {
16
16
  return process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
17
17
  }
18
+
19
+ /**
20
+ * Gets the custom pip registries from environment variable
21
+ * Expected format: comma-separated list of registry domains
22
+ * Example: "pip.company.com,registry.internal.net"
23
+ * @returns {string | undefined}
24
+ */
25
+ export function getPipCustomRegistries() {
26
+ return process.env.SAFE_CHAIN_PIP_CUSTOM_REGISTRIES;
27
+ }
@@ -143,3 +143,21 @@ export function getNpmCustomRegistries() {
143
143
  // Normalize each registry (remove protocol if any)
144
144
  return uniqueRegistries.map(normalizeRegistry);
145
145
  }
146
+
147
+ /**
148
+ * Gets the custom npm registries from both environment variable and config file (merged)
149
+ * @returns {string[]}
150
+ */
151
+ export function getPipCustomRegistries() {
152
+ const envRegistries = parseRegistriesFromEnv(
153
+ environmentVariables.getPipCustomRegistries()
154
+ );
155
+ const configRegistries = configFile.getPipCustomRegistries();
156
+
157
+ // Merge both sources and remove duplicates
158
+ const allRegistries = [...envRegistries, ...configRegistries];
159
+ const uniqueRegistries = [...new Set(allRegistries)];
160
+
161
+ // Normalize each registry (remove protocol if any)
162
+ return uniqueRegistries.map(normalizeRegistry);
163
+ }
package/src/main.js CHANGED
@@ -13,6 +13,10 @@ import { getAuditStats } from "./scanning/audit/index.js";
13
13
  * @returns {Promise<number>}
14
14
  */
15
15
  export async function main(args) {
16
+ if (isSafeChainVerify(args)) {
17
+ return 0;
18
+ }
19
+
16
20
  process.on("SIGINT", handleProcessTermination);
17
21
  process.on("SIGTERM", handleProcessTermination);
18
22
 
@@ -104,3 +108,12 @@ export async function main(args) {
104
108
  function handleProcessTermination() {
105
109
  ui.writeBufferedLogsAndStopBuffering();
106
110
  }
111
+
112
+ /** @param {string[]} args */
113
+ function isSafeChainVerify(args) {
114
+ const safeChainCheckCommand = "safe-chain-verify";
115
+ if (args.length > 0 && args[0] === safeChainCheckCommand) {
116
+ ui.writeInformation("OK: Safe-chain works!");
117
+ return true;
118
+ }
119
+ }
@@ -1,3 +1,4 @@
1
+ import { getPipCustomRegistries } from "../../config/settings.js";
1
2
  import { isMalwarePackage } from "../../scanning/audit/index.js";
2
3
  import { interceptRequests } from "./interceptorBuilder.js";
3
4
 
@@ -13,7 +14,9 @@ const knownPipRegistries = [
13
14
  * @returns {import("./interceptorBuilder.js").Interceptor | undefined}
14
15
  */
15
16
  export function pipInterceptorForUrl(url) {
16
- const registry = knownPipRegistries.find((reg) => url.includes(reg));
17
+ const customRegistries = getPipCustomRegistries();
18
+ const registries = [...knownPipRegistries, ...customRegistries];
19
+ const registry = registries.find((reg) => url.includes(reg));
17
20
 
18
21
  if (registry) {
19
22
  return buildPipInterceptor(registry);
@@ -37,8 +40,8 @@ function buildPipInterceptor(registry) {
37
40
  // Per python, packages that differ only by hyphen vs underscore are considered the same.
38
41
  const hyphenName = packageName?.includes("_") ? packageName.replace(/_/g, "-") : packageName;
39
42
 
40
- const isMalicious =
41
- await isMalwarePackage(packageName, version)
43
+ const isMalicious =
44
+ await isMalwarePackage(packageName, version)
42
45
  || await isMalwarePackage(hyphenName, version);
43
46
 
44
47
  if (isMalicious) {
@@ -15,7 +15,7 @@ import { gunzipSync, gzipSync } from "zlib";
15
15
  */
16
16
  export function mitmConnect(req, clientSocket, interceptor) {
17
17
  ui.writeVerbose(`Safe-chain: Set up MITM tunnel for ${req.url}`);
18
- const { hostname } = new URL(`http://${req.url}`);
18
+ const { hostname, port } = new URL(`http://${req.url}`);
19
19
 
20
20
  clientSocket.on("error", (err) => {
21
21
  ui.writeVerbose(
@@ -26,7 +26,7 @@ export function mitmConnect(req, clientSocket, interceptor) {
26
26
  // Not subscribing to 'close' event will cause node to throw and crash.
27
27
  });
28
28
 
29
- const server = createHttpsServer(hostname, interceptor);
29
+ const server = createHttpsServer(hostname, port, interceptor);
30
30
 
31
31
  server.on("error", (err) => {
32
32
  ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`);
@@ -46,10 +46,11 @@ export function mitmConnect(req, clientSocket, interceptor) {
46
46
 
47
47
  /**
48
48
  * @param {string} hostname
49
+ * @param {string} port
49
50
  * @param {Interceptor} interceptor
50
51
  * @returns {import("https").Server}
51
52
  */
52
- function createHttpsServer(hostname, interceptor) {
53
+ function createHttpsServer(hostname, port, interceptor) {
53
54
  const cert = generateCertForHost(hostname);
54
55
 
55
56
  /**
@@ -80,7 +81,7 @@ function createHttpsServer(hostname, interceptor) {
80
81
  }
81
82
 
82
83
  // Collect request body
83
- forwardRequest(req, hostname, res, requestInterceptor);
84
+ forwardRequest(req, hostname, port, res, requestInterceptor);
84
85
  }
85
86
 
86
87
  const server = https.createServer(
@@ -109,11 +110,12 @@ function getRequestPathAndQuery(url) {
109
110
  /**
110
111
  * @param {import("http").IncomingMessage} req
111
112
  * @param {string} hostname
113
+ * @param {string} port
112
114
  * @param {import("http").ServerResponse} res
113
115
  * @param {import("./interceptors/interceptorBuilder.js").RequestInterceptionHandler} requestHandler
114
116
  */
115
- function forwardRequest(req, hostname, res, requestHandler) {
116
- const proxyReq = createProxyRequest(hostname, req, res, requestHandler);
117
+ function forwardRequest(req, hostname, port, res, requestHandler) {
118
+ const proxyReq = createProxyRequest(hostname, port, req, res, requestHandler);
117
119
 
118
120
  proxyReq.on("error", (err) => {
119
121
  ui.writeVerbose(
@@ -144,13 +146,14 @@ function forwardRequest(req, hostname, res, requestHandler) {
144
146
 
145
147
  /**
146
148
  * @param {string} hostname
149
+ * @param {string} port
147
150
  * @param {import("http").IncomingMessage} req
148
151
  * @param {import("http").ServerResponse} res
149
152
  * @param {import("./interceptors/interceptorBuilder.js").RequestInterceptionHandler} requestHandler
150
153
  *
151
154
  * @returns {import("http").ClientRequest}
152
155
  */
153
- function createProxyRequest(hostname, req, res, requestHandler) {
156
+ function createProxyRequest(hostname, port, req, res, requestHandler) {
154
157
  /** @type {NodeJS.Dict<string | string[]> | undefined} */
155
158
  let headers = { ...req.headers };
156
159
  // Remove the host header from the incoming request before forwarding.
@@ -163,7 +166,7 @@ function createProxyRequest(hostname, req, res, requestHandler) {
163
166
  /** @type {import("http").RequestOptions} */
164
167
  const options = {
165
168
  hostname: hostname,
166
- port: 443,
169
+ port: port || 443,
167
170
  path: req.url,
168
171
  method: req.method,
169
172
  headers: { ...headers },