@aikidosec/safe-chain 1.5.0 → 1.5.2

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
@@ -10,6 +10,14 @@
10
10
  - ✅ **Blocks packages newer than 48 hours** without breaking your build
11
11
  - ✅ **Tokenless, free, no build data shared**
12
12
 
13
+ ## Need protection beyond npm & PyPI?
14
+
15
+ [Aikido Endpoint](https://www.aikido.dev/protect/endpoint-protection?utm_source=github.com&utm_medium=referral&utm_campaign=safechain) builds on Safe Chain, extending package and extension security across more ecosystems: **npm**, **PyPI**, **Maven**, **NuGet**, **VS Code**, **Open VSX** - (Cursor, Windsurf, Kiro, Vs Codium, ...), **Chrome extensions**, **Skills.sh AI skills** and more.
16
+
17
+ Get centralized policy management, request-and-approval workflows, and visibility across every developer workstation in your org. Powered by the same Aikido Intel feed. Deploy it manually or manage it through your MDM tool (Jamf, Fleet, or Iru).
18
+
19
+ ---
20
+
13
21
  Aikido Safe Chain supports the following package managers:
14
22
 
15
23
  - 📦 **npm**
@@ -282,6 +290,12 @@ You can set custom registries through environment variable or config file. Both
282
290
  }
283
291
  ```
284
292
 
293
+ ## PYPI Configuration File
294
+
295
+ If you rely on a `pip.conf` file for pip configuration you must point pip at it explicitly via the `PIP_CONFIG_FILE` environment variable so Safe Chain can merge it.
296
+
297
+ Safe Chain runs pip behind its MITM proxy and writes a temporary pip configuration file to inject its certificate and proxy settings. When `PIP_CONFIG_FILE` is set, Safe Chain merges its settings into a copy of your file (your original file is never modified) so your `index-url`, credentials, and other options are preserved. When `PIP_CONFIG_FILE` is not set, pip's user-level config (e.g. `~/.config/pip/pip.conf`) might be overridden by Safe Chain's temporary file and your settings will not be picked up.
298
+
285
299
  ## Malware List Base URL
286
300
 
287
301
  Configure Safe Chain to fetch malware databases and new packages lists from a custom mirror URL. This allows you to host your own copy of the Aikido malware database.
@@ -463,7 +477,7 @@ steps:
463
477
  name: Install
464
478
  script:
465
479
  - curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
466
- - export PATH=~/.safe-chain/shims:$PATH
480
+ - export PATH=~/.safe-chain/shims:~/.safe-chain/bin:$PATH
467
481
  - npm ci
468
482
  ```
469
483
 
@@ -3112,7 +3112,7 @@
3112
3112
  },
3113
3113
  "packages/safe-chain": {
3114
3114
  "name": "@aikidosec/safe-chain",
3115
- "version": "1.5.0",
3115
+ "version": "1.5.2",
3116
3116
  "license": "AGPL-3.0-or-later",
3117
3117
  "dependencies": {
3118
3118
  "certifi": "14.5.15",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikidosec/safe-chain",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
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'",
@@ -42,7 +42,7 @@ function getSafeChainProxyEnvironmentVariables() {
42
42
  return {};
43
43
  }
44
44
 
45
- const proxyUrl = `http://localhost:${state.port}`;
45
+ const proxyUrl = `http://127.0.0.1:${state.port}`;
46
46
  const caCertPath = getCombinedCaBundlePath();
47
47
 
48
48
  return {
@@ -95,8 +95,11 @@ function createProxyServer() {
95
95
  */
96
96
  function startServer(server) {
97
97
  return new Promise((resolve, reject) => {
98
- // Passing port 0 makes the OS assign an available port
99
- server.listen(0, () => {
98
+ // Bind to loopback only. Without an explicit host, Node listens on every
99
+ // interface, turning the proxy into an unauthenticated forward proxy that
100
+ // anyone reachable on the network can use to hit the victim's localhost,
101
+ // intranet, or cloud metadata endpoints. Port 0 lets the OS pick a port.
102
+ server.listen(0, "127.0.0.1", () => {
100
103
  const address = server.address();
101
104
  if (address && typeof address === "object") {
102
105
  state.port = address.port;
@@ -15,8 +15,12 @@ import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js";
15
15
  * @property {function(string, string): boolean} isMalware
16
16
  */
17
17
 
18
- /** @type {MalwareDatabase | null} */
19
- let cachedMalwareDatabase = null;
18
+ // Caching the Promise (rather than the resolved database) prevents duplicate fetches. If we cached the resolved
19
+ // value, multiple callers could pass the null-check before the first fetch completes (because each `await` yields
20
+ // control back to the event loop, allowing other callers to run). Since the Promise assignment is synchronous, all
21
+ // concurrent callers see it immediately and share a single fetch.
22
+ /** @type {Promise<MalwareDatabase> | null} */
23
+ let cachedMalwareDatabasePromise = null;
20
24
 
21
25
  /**
22
26
  * Normalize package name for comparison.
@@ -34,45 +38,44 @@ function normalizePackageName(name) {
34
38
  return name;
35
39
  }
36
40
 
37
- export async function openMalwareDatabase() {
38
- if (cachedMalwareDatabase) {
39
- return cachedMalwareDatabase;
40
- }
41
-
42
- const malwareDatabase = await getMalwareDatabase();
43
-
44
- /**
45
- * @param {string} name
46
- * @param {string} version
47
- * @returns {string}
48
- */
49
- function getPackageStatus(name, version) {
50
- const normalizedName = normalizePackageName(name);
51
- const packageData = malwareDatabase.find(
52
- (pkg) => {
53
- const normalizedPkgName = normalizePackageName(pkg.package_name);
54
- return normalizedPkgName === normalizedName &&
55
- (pkg.version === version || pkg.version === "*");
41
+ export function openMalwareDatabase() {
42
+ if (!cachedMalwareDatabasePromise) {
43
+ cachedMalwareDatabasePromise = getMalwareDatabase().then((malwareDatabase) => {
44
+ /**
45
+ * @param {string} name
46
+ * @param {string} version
47
+ * @returns {string}
48
+ */
49
+ function getPackageStatus(name, version) {
50
+ const normalizedName = normalizePackageName(name);
51
+ const packageData = malwareDatabase.find(
52
+ (pkg) => {
53
+ const normalizedPkgName = normalizePackageName(pkg.package_name);
54
+ return normalizedPkgName === normalizedName &&
55
+ (pkg.version === version || pkg.version === "*");
56
+ }
57
+ );
58
+
59
+ if (!packageData) {
60
+ return MALWARE_STATUS_OK;
61
+ }
62
+
63
+ return packageData.reason;
56
64
  }
57
- );
58
65
 
59
- if (!packageData) {
60
- return MALWARE_STATUS_OK;
61
- }
62
-
63
- return packageData.reason;
66
+ return {
67
+ getPackageStatus,
68
+ isMalware: (/** @type {string} */ name, /** @type {string} */ version) => {
69
+ const status = getPackageStatus(name, version);
70
+ return isMalwareStatus(status);
71
+ },
72
+ };
73
+ }).catch((error) => {
74
+ cachedMalwareDatabasePromise = null;
75
+ throw error;
76
+ });
64
77
  }
65
-
66
- // This implicitly caches the malware database
67
- // that's closed over by the getPackageStatus function
68
- cachedMalwareDatabase = {
69
- getPackageStatus,
70
- isMalware: (name, version) => {
71
- const status = getPackageStatus(name, version);
72
- return isMalwareStatus(status);
73
- },
74
- };
75
- return cachedMalwareDatabase;
78
+ return cachedMalwareDatabasePromise;
76
79
  }
77
80
 
78
81
  /**
@@ -16,30 +16,27 @@ import { warnOnceAboutUnavailableDatabase } from "./newPackagesDatabaseWarnings.
16
16
  */
17
17
 
18
18
  // Shared per-process cache to avoid rebuilding the same feed-backed database on each request.
19
- /** @type {NewPackagesDatabase | null} */
20
- let cachedNewPackagesDatabase = null;
19
+ // Caching the Promise (rather than the resolved database) prevents duplicate fetches. If we cached the resolved
20
+ // value, multiple callers could pass the null-check before the first fetch completes (because each `await` yields
21
+ // control back to the event loop, allowing other callers to run). Since the Promise assignment is synchronous, all
22
+ // concurrent callers see it immediately and share a single fetch.
23
+ /** @type {Promise<NewPackagesDatabase> | null} */
24
+ let cachedNewPackagesDatabasePromise = null;
21
25
 
22
26
  /**
23
27
  * @returns {Promise<NewPackagesDatabase>}
24
28
  */
25
- export async function openNewPackagesDatabase() {
26
- if (cachedNewPackagesDatabase) {
27
- return cachedNewPackagesDatabase;
29
+ export function openNewPackagesDatabase() {
30
+ if (!cachedNewPackagesDatabasePromise) {
31
+ cachedNewPackagesDatabasePromise = getNewPackagesList()
32
+ .then((newPackagesList) => buildNewPackagesDatabase(newPackagesList))
33
+ .catch((/** @type {any} */ error) => {
34
+ warnOnceAboutUnavailableDatabase(error);
35
+ cachedNewPackagesDatabasePromise = null;
36
+ return { isNewlyReleasedPackage: () => false };
37
+ });
28
38
  }
29
-
30
- /** @type {import("../api/aikido.js").NewPackageEntry[]} */
31
- let newPackagesList;
32
-
33
- try {
34
- newPackagesList = await getNewPackagesList();
35
- } catch (/** @type {any} */ error) {
36
- warnOnceAboutUnavailableDatabase(error);
37
- cachedNewPackagesDatabase = { isNewlyReleasedPackage: () => false };
38
- return cachedNewPackagesDatabase;
39
- }
40
-
41
- cachedNewPackagesDatabase = buildNewPackagesDatabase(newPackagesList);
42
- return cachedNewPackagesDatabase;
39
+ return cachedNewPackagesDatabasePromise;
43
40
  }
44
41
 
45
42
  /**