@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
|
|
package/npm-shrinkwrap.json
CHANGED
package/package.json
CHANGED
|
@@ -42,7 +42,7 @@ function getSafeChainProxyEnvironmentVariables() {
|
|
|
42
42
|
return {};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const proxyUrl = `http://
|
|
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
|
-
//
|
|
99
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
26
|
-
if (
|
|
27
|
-
|
|
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
|
/**
|