@aikidosec/safe-chain 1.4.9 → 1.5.1
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 +34 -6
- package/bin/aikido-uvx.js +16 -0
- package/bin/safe-chain.js +18 -1
- package/docs/Release.md +25 -0
- package/docs/shell-integration.md +4 -4
- package/npm-shrinkwrap.json +53 -942
- package/package.json +3 -4
- package/src/config/configFile.js +2 -2
- package/src/config/safeChainDir.js +71 -0
- package/src/installLocation.js +42 -0
- package/src/packagemanager/currentPackageManager.js +3 -0
- package/src/packagemanager/uvx/createUvxPackageManager.js +18 -0
- package/src/registryProxy/certUtils.js +3 -3
- package/src/registryProxy/interceptors/pip/modifyPipInfo.js +17 -0
- package/src/registryProxy/interceptors/pip/pipInterceptor.js +2 -0
- package/src/scanning/malwareDatabase.js +41 -38
- package/src/scanning/newPackagesListCache.js +16 -19
- package/src/shell-integration/helpers.js +6 -14
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +16 -1
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +3 -2
- package/src/shell-integration/setup-ci.js +9 -29
- package/src/shell-integration/setup.js +3 -22
- package/src/shell-integration/startup-scripts/init-fish.fish +8 -1
- package/src/shell-integration/startup-scripts/init-posix.sh +17 -1
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +6 -1
- package/src/shell-integration/supported-shells/bash.js +75 -5
- package/src/shell-integration/supported-shells/fish.js +7 -5
- package/src/shell-integration/supported-shells/powershell.js +7 -5
- package/src/shell-integration/supported-shells/windowsPowershell.js +7 -5
- package/src/shell-integration/supported-shells/zsh.js +7 -5
- package/src/shell-integration/teardown.js +3 -1
- package/src/ultimate/ultimateTroubleshooting.js +0 -111
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikidosec/safe-chain",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
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'",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"aikido-bun": "bin/aikido-bun.js",
|
|
17
17
|
"aikido-bunx": "bin/aikido-bunx.js",
|
|
18
18
|
"aikido-uv": "bin/aikido-uv.js",
|
|
19
|
+
"aikido-uvx": "bin/aikido-uvx.js",
|
|
19
20
|
"aikido-pip": "bin/aikido-pip.js",
|
|
20
21
|
"aikido-pip3": "bin/aikido-pip3.js",
|
|
21
22
|
"aikido-python": "bin/aikido-python.js",
|
|
@@ -36,9 +37,8 @@
|
|
|
36
37
|
"keywords": [],
|
|
37
38
|
"author": "Aikido Security",
|
|
38
39
|
"license": "AGPL-3.0-or-later",
|
|
39
|
-
"description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, or pip/pip3 from downloading or running the malware.",
|
|
40
|
+
"description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), and [pip](https://pip.pypa.io/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, bun, bunx, uv, uvx, or pip/pip3 from downloading or running the malware.",
|
|
40
41
|
"dependencies": {
|
|
41
|
-
"archiver": "^7.0.1",
|
|
42
42
|
"certifi": "14.5.15",
|
|
43
43
|
"chalk": "5.4.1",
|
|
44
44
|
"https-proxy-agent": "7.0.6",
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
"semver": "7.7.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@types/archiver": "^7.0.0",
|
|
53
52
|
"@types/ini": "^4.1.1",
|
|
54
53
|
"@types/make-fetch-happen": "^10.0.4",
|
|
55
54
|
"@types/node": "^18.19.130",
|
package/src/config/configFile.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "path";
|
|
|
3
3
|
import os from "os";
|
|
4
4
|
import { ui } from "../environment/userInteraction.js";
|
|
5
5
|
import { getEcoSystem } from "./settings.js";
|
|
6
|
+
import { getSafeChainBaseDir } from "./safeChainDir.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @typedef {Object} SafeChainConfig
|
|
@@ -304,8 +305,7 @@ function getConfigFilePath() {
|
|
|
304
305
|
* @returns {string}
|
|
305
306
|
*/
|
|
306
307
|
export function getSafeChainDirectory() {
|
|
307
|
-
const
|
|
308
|
-
const safeChainDir = path.join(homeDir, ".safe-chain");
|
|
308
|
+
const safeChainDir = getSafeChainBaseDir();
|
|
309
309
|
|
|
310
310
|
if (!fs.existsSync(safeChainDir)) {
|
|
311
311
|
fs.mkdirSync(safeChainDir, { recursive: true });
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { getInstalledSafeChainDir } from "../installLocation.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function getSafeChainBaseDir() {
|
|
10
|
+
return getInstalledSafeChainDir() ?? path.join(os.homedir(), ".safe-chain");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
export function getBinDir() {
|
|
17
|
+
return path.join(getSafeChainBaseDir(), "bin");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function getShimsDir() {
|
|
24
|
+
return path.join(getSafeChainBaseDir(), "shims");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function getScriptsDir() {
|
|
31
|
+
return path.join(getSafeChainBaseDir(), "scripts");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function getCertsDir() {
|
|
38
|
+
return path.join(getSafeChainBaseDir(), "certs");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the directory of the calling module.
|
|
43
|
+
* Falls back to __dirname when import.meta.url is unavailable (pkg CJS binary).
|
|
44
|
+
* @param {string | undefined} moduleUrl
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function resolveModuleDir(moduleUrl) {
|
|
48
|
+
if (moduleUrl) {
|
|
49
|
+
return path.dirname(fileURLToPath(moduleUrl));
|
|
50
|
+
}
|
|
51
|
+
// eslint-disable-next-line no-undef
|
|
52
|
+
return __dirname;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string | undefined} moduleUrl
|
|
57
|
+
* @param {string} fileName
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function getStartupScriptSourcePath(moduleUrl, fileName) {
|
|
61
|
+
return path.join(resolveModuleDir(moduleUrl), "startup-scripts", fileName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {string | undefined} moduleUrl
|
|
66
|
+
* @param {string} fileName
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
export function getPathWrapperTemplatePath(moduleUrl, fileName) {
|
|
70
|
+
return path.join(resolveModuleDir(moduleUrl), "path-wrappers", "templates", fileName);
|
|
71
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
/** @type {NodeJS.Process & { pkg?: unknown }} */
|
|
4
|
+
const processWithPkg = process;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} executablePath
|
|
8
|
+
* @returns {string | undefined}
|
|
9
|
+
*/
|
|
10
|
+
export function deriveInstallDirFromExecutablePath(executablePath) {
|
|
11
|
+
if (!executablePath) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const pathLibrary = executablePath.includes("\\") ? path.win32 : path.posix;
|
|
16
|
+
const executableDir = pathLibrary.dirname(executablePath);
|
|
17
|
+
if (pathLibrary.basename(executableDir) !== "bin") {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return pathLibrary.dirname(executableDir);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns the install directory for a packaged safe-chain binary.
|
|
26
|
+
* Custom installation directories only apply to packaged binary installs.
|
|
27
|
+
* For npm/global/dev-script executions this intentionally returns undefined,
|
|
28
|
+
* which causes callers to fall back to the default ~/.safe-chain layout.
|
|
29
|
+
*
|
|
30
|
+
* @param {{ isPackaged?: boolean, executablePath?: string }} [options]
|
|
31
|
+
* @returns {string | undefined}
|
|
32
|
+
*/
|
|
33
|
+
export function getInstalledSafeChainDir(options = {}) {
|
|
34
|
+
const isPackaged = options.isPackaged ?? Boolean(processWithPkg.pkg);
|
|
35
|
+
if (!isPackaged) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return deriveInstallDirFromExecutablePath(
|
|
40
|
+
options.executablePath ?? process.execPath,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -13,6 +13,7 @@ import { createPipPackageManager } from "./pip/createPackageManager.js";
|
|
|
13
13
|
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
|
|
14
14
|
import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
|
|
15
15
|
import { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
|
|
16
|
+
import { createUvxPackageManager } from "./uvx/createUvxPackageManager.js";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* @type {{packageManagerName: PackageManager | null}}
|
|
@@ -60,6 +61,8 @@ export function initializePackageManager(packageManagerName, context) {
|
|
|
60
61
|
state.packageManagerName = createPipPackageManager(context);
|
|
61
62
|
} else if (packageManagerName === "uv") {
|
|
62
63
|
state.packageManagerName = createUvPackageManager();
|
|
64
|
+
} else if (packageManagerName === "uvx") {
|
|
65
|
+
state.packageManagerName = createUvxPackageManager();
|
|
63
66
|
} else if (packageManagerName === "poetry") {
|
|
64
67
|
state.packageManagerName = createPoetryPackageManager();
|
|
65
68
|
} else if (packageManagerName === "pipx") {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { runUv } from "../uv/runUvCommand.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @returns {import("../currentPackageManager.js").PackageManager}
|
|
5
|
+
*/
|
|
6
|
+
export function createUvxPackageManager() {
|
|
7
|
+
return {
|
|
8
|
+
/**
|
|
9
|
+
* @param {string[]} args
|
|
10
|
+
*/
|
|
11
|
+
runCommand: (args) => {
|
|
12
|
+
return runUv("uvx", args);
|
|
13
|
+
},
|
|
14
|
+
// For uvx, rely solely on MITM
|
|
15
|
+
isSupportedCommand: () => false,
|
|
16
|
+
getDependencyUpdatesForCommand: () => [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import forge from "node-forge";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import fs from "fs";
|
|
4
|
-
import
|
|
4
|
+
import { getCertsDir } from "../config/safeChainDir.js";
|
|
5
5
|
|
|
6
|
-
const certFolder = path.join(os.homedir(), ".safe-chain", "certs");
|
|
7
6
|
const ca = loadCa();
|
|
8
7
|
|
|
9
8
|
const certCache = new Map();
|
|
@@ -20,7 +19,7 @@ function createKeyIdentifier(publicKey) {
|
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export function getCaCertPath() {
|
|
23
|
-
return path.join(
|
|
22
|
+
return path.join(getCertsDir(), "ca-cert.pem");
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
/**
|
|
@@ -112,6 +111,7 @@ export function generateCertForHost(hostname) {
|
|
|
112
111
|
}
|
|
113
112
|
|
|
114
113
|
function loadCa() {
|
|
114
|
+
const certFolder = getCertsDir();
|
|
115
115
|
const keyPath = path.join(certFolder, "ca-key.pem");
|
|
116
116
|
const certPath = path.join(certFolder, "ca-cert.pem");
|
|
117
117
|
|
|
@@ -6,6 +6,23 @@ export { parsePipMetadataUrl, isPipPackageInfoUrl } from "./parsePipPackageUrl.j
|
|
|
6
6
|
import { getPipMetadataContentType, logSuppressedVersion } from "./pipMetadataResponseUtils.js";
|
|
7
7
|
import { modifyPipJsonResponse } from "./modifyPipJsonResponse.js";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Strip conditional GET headers so PyPI always returns a full 200 response
|
|
11
|
+
* with a body we can rewrite. Without this, pip sends If-None-Match /
|
|
12
|
+
* If-Modified-Since, PyPI responds 304 Not Modified (empty body), and
|
|
13
|
+
* safe-chain cannot rewrite it — leaving pip with a cached index that still
|
|
14
|
+
* lists too-young versions. Those versions are then blocked at direct-download
|
|
15
|
+
* time with a hard 403, preventing dependency resolution from completing.
|
|
16
|
+
*
|
|
17
|
+
* @param {NodeJS.Dict<string | string[]>} headers
|
|
18
|
+
* @returns {NodeJS.Dict<string | string[]>}
|
|
19
|
+
*/
|
|
20
|
+
export function modifyPipInfoRequestHeaders(headers) {
|
|
21
|
+
delete headers["if-none-match"];
|
|
22
|
+
delete headers["if-modified-since"];
|
|
23
|
+
return headers;
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
// Match simple-index anchor tags and capture their href so we can suppress
|
|
10
27
|
// individual distribution links from PyPI HTML metadata responses.
|
|
11
28
|
const HTML_ANCHOR_HREF_RE =
|
|
@@ -9,6 +9,7 @@ import { openNewPackagesDatabase } from "../../../scanning/newPackagesListCache.
|
|
|
9
9
|
import { interceptRequests } from "../interceptorBuilder.js";
|
|
10
10
|
import { isExcludedFromMinimumPackageAge } from "../minimumPackageAgeExclusions.js";
|
|
11
11
|
import {
|
|
12
|
+
modifyPipInfoRequestHeaders,
|
|
12
13
|
modifyPipInfoResponse,
|
|
13
14
|
parsePipMetadataUrl,
|
|
14
15
|
} from "./modifyPipInfo.js";
|
|
@@ -61,6 +62,7 @@ function createPipRequestHandler(registry) {
|
|
|
61
62
|
!isExcludedFromMinimumPackageAge(metadataPackageName)
|
|
62
63
|
) {
|
|
63
64
|
const newPackagesDatabase = await openNewPackagesDatabase();
|
|
65
|
+
reqContext.modifyRequestHeaders(modifyPipInfoRequestHeaders);
|
|
64
66
|
reqContext.modifyBody((body, headers) =>
|
|
65
67
|
modifyPipInfoResponse(
|
|
66
68
|
body,
|
|
@@ -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
|
/**
|
|
@@ -66,6 +66,12 @@ export const knownAikidoTools = [
|
|
|
66
66
|
ecoSystem: ECOSYSTEM_PY,
|
|
67
67
|
internalPackageManagerName: "uv",
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
tool: "uvx",
|
|
71
|
+
aikidoCommand: "aikido-uvx",
|
|
72
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
73
|
+
internalPackageManagerName: "uvx",
|
|
74
|
+
},
|
|
69
75
|
{
|
|
70
76
|
tool: "pip",
|
|
71
77
|
aikidoCommand: "aikido-pip",
|
|
@@ -121,20 +127,6 @@ export function getPackageManagerList() {
|
|
|
121
127
|
return `${tools.join(", ")}, and ${lastTool} commands`;
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
/**
|
|
125
|
-
* @returns {string}
|
|
126
|
-
*/
|
|
127
|
-
export function getShimsDir() {
|
|
128
|
-
return path.join(os.homedir(), ".safe-chain", "shims");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* @returns {string}
|
|
133
|
-
*/
|
|
134
|
-
export function getScriptsDir() {
|
|
135
|
-
return path.join(os.homedir(), ".safe-chain", "scripts");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
130
|
/**
|
|
139
131
|
* @param {string} executableName
|
|
140
132
|
*
|
|
@@ -4,13 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
# Function to remove shim from PATH (POSIX-compliant)
|
|
6
6
|
remove_shim_from_path() {
|
|
7
|
-
|
|
7
|
+
_safe_chain_phys=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd -P)
|
|
8
|
+
if [ -z "$_safe_chain_phys" ]; then
|
|
9
|
+
echo "$PATH"
|
|
10
|
+
return
|
|
11
|
+
fi
|
|
12
|
+
_path=$(echo "$PATH" | sed "s|${_safe_chain_phys}:||g")
|
|
13
|
+
# Also remove via dirname of $0 directly — on macOS /tmp is a symlink to /private/tmp,
|
|
14
|
+
# so pwd -P resolves to /private/tmp/… but PATH may still contain /tmp/….
|
|
15
|
+
_dir=$(dirname -- "$0")
|
|
16
|
+
case "$_dir" in
|
|
17
|
+
/*) [ "$_dir" != "$_safe_chain_phys" ] && _path=$(echo "$_path" | sed "s|${_dir}:||g") ;;
|
|
18
|
+
esac
|
|
19
|
+
echo "$_path"
|
|
8
20
|
}
|
|
9
21
|
|
|
10
22
|
if command -v safe-chain >/dev/null 2>&1; then
|
|
11
23
|
# Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops
|
|
12
24
|
PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
|
|
13
25
|
else
|
|
26
|
+
# safe-chain is not reachable — warn the user so they know protection is inactive
|
|
27
|
+
printf "\033[43;30mWarning:\033[0m safe-chain is not available to protect you from installing malware. {{PACKAGE_MANAGER}} will run without it.\n" >&2
|
|
28
|
+
|
|
14
29
|
# Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory)
|
|
15
30
|
original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}})
|
|
16
31
|
if [ -n "$original_cmd" ]; then
|
|
@@ -3,7 +3,8 @@ REM Generated wrapper for {{PACKAGE_MANAGER}} by safe-chain
|
|
|
3
3
|
REM This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments
|
|
4
4
|
|
|
5
5
|
REM Remove shim directory from PATH to prevent infinite loops
|
|
6
|
-
set "SHIM_DIR
|
|
6
|
+
set "SHIM_DIR=%~dp0"
|
|
7
|
+
if "%SHIM_DIR:~-1%"=="\" set "SHIM_DIR=%SHIM_DIR:~0,-1%"
|
|
7
8
|
call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
|
|
8
9
|
|
|
9
10
|
REM Check if aikido command is available with clean PATH
|
|
@@ -21,4 +22,4 @@ if %errorlevel%==0 (
|
|
|
21
22
|
REM If we get here, original command was not found
|
|
22
23
|
echo Error: Could not find original {{PACKAGE_MANAGER}} >&2
|
|
23
24
|
exit /b 1
|
|
24
|
-
)
|
|
25
|
+
)
|
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { ui } from "../environment/userInteraction.js";
|
|
3
|
-
import { getPackageManagerList, knownAikidoTools
|
|
3
|
+
import { getPackageManagerList, knownAikidoTools } from "./helpers.js";
|
|
4
|
+
import {
|
|
5
|
+
getShimsDir,
|
|
6
|
+
getBinDir,
|
|
7
|
+
getPathWrapperTemplatePath,
|
|
8
|
+
} from "../config/safeChainDir.js";
|
|
4
9
|
import fs from "fs";
|
|
5
10
|
import os from "os";
|
|
6
11
|
import path from "path";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
|
-
|
|
9
|
-
/** @type {string} */
|
|
10
|
-
// This checks the current file's dirname in a way that's compatible with:
|
|
11
|
-
// - Modulejs (import.meta.url)
|
|
12
|
-
// - ES modules (__dirname)
|
|
13
|
-
// This is needed because safe-chain's npm package is built using ES modules,
|
|
14
|
-
// but building the binaries requires commonjs.
|
|
15
|
-
let dirname;
|
|
16
|
-
if (import.meta.url) {
|
|
17
|
-
const filename = fileURLToPath(import.meta.url);
|
|
18
|
-
dirname = path.dirname(filename);
|
|
19
|
-
} else {
|
|
20
|
-
dirname = __dirname;
|
|
21
|
-
}
|
|
22
12
|
|
|
23
13
|
/**
|
|
24
14
|
* Loops over the detected shells and calls the setup function for each.
|
|
@@ -31,7 +21,7 @@ export async function setupCi() {
|
|
|
31
21
|
ui.emptyLine();
|
|
32
22
|
|
|
33
23
|
const shimsDir = getShimsDir();
|
|
34
|
-
const binDir =
|
|
24
|
+
const binDir = getBinDir();
|
|
35
25
|
// Create the shims directory if it doesn't exist
|
|
36
26
|
if (!fs.existsSync(shimsDir)) {
|
|
37
27
|
fs.mkdirSync(shimsDir, { recursive: true });
|
|
@@ -50,12 +40,7 @@ export async function setupCi() {
|
|
|
50
40
|
*/
|
|
51
41
|
function createUnixShims(shimsDir) {
|
|
52
42
|
// Read the template file
|
|
53
|
-
const templatePath =
|
|
54
|
-
dirname,
|
|
55
|
-
"path-wrappers",
|
|
56
|
-
"templates",
|
|
57
|
-
"unix-wrapper.template.sh"
|
|
58
|
-
);
|
|
43
|
+
const templatePath = getPathWrapperTemplatePath(import.meta.url, "unix-wrapper.template.sh");
|
|
59
44
|
|
|
60
45
|
if (!fs.existsSync(templatePath)) {
|
|
61
46
|
ui.writeError(`Template file not found: ${templatePath}`);
|
|
@@ -89,12 +74,7 @@ function createUnixShims(shimsDir) {
|
|
|
89
74
|
*/
|
|
90
75
|
function createWindowsShims(shimsDir) {
|
|
91
76
|
// Read the template file
|
|
92
|
-
const templatePath =
|
|
93
|
-
dirname,
|
|
94
|
-
"path-wrappers",
|
|
95
|
-
"templates",
|
|
96
|
-
"windows-wrapper.template.cmd"
|
|
97
|
-
);
|
|
77
|
+
const templatePath = getPathWrapperTemplatePath(import.meta.url, "windows-wrapper.template.cmd");
|
|
98
78
|
|
|
99
79
|
if (!fs.existsSync(templatePath)) {
|
|
100
80
|
ui.writeError(`Windows template file not found: ${templatePath}`);
|
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { ui } from "../environment/userInteraction.js";
|
|
3
3
|
import { detectShells } from "./shellDetection.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
getPackageManagerList,
|
|
7
|
-
getScriptsDir,
|
|
8
|
-
} from "./helpers.js";
|
|
4
|
+
import { knownAikidoTools, getPackageManagerList } from "./helpers.js";
|
|
5
|
+
import { getScriptsDir, getStartupScriptSourcePath } from "../config/safeChainDir.js";
|
|
9
6
|
import fs from "fs";
|
|
10
7
|
import path from "path";
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
12
|
-
|
|
13
|
-
/** @type {string} */
|
|
14
|
-
// This checks the current file's dirname in a way that's compatible with:
|
|
15
|
-
// - Modulejs (import.meta.url)
|
|
16
|
-
// - ES modules (__dirname)
|
|
17
|
-
// This is needed because safe-chain's npm package is built using ES modules,
|
|
18
|
-
// but building the binaries requires commonjs.
|
|
19
|
-
let dirname;
|
|
20
|
-
if (import.meta.url) {
|
|
21
|
-
const filename = fileURLToPath(import.meta.url);
|
|
22
|
-
dirname = path.dirname(filename);
|
|
23
|
-
} else {
|
|
24
|
-
dirname = __dirname;
|
|
25
|
-
}
|
|
26
8
|
|
|
27
9
|
/**
|
|
28
10
|
* Loops over the detected shells and calls the setup function for each.
|
|
@@ -122,8 +104,7 @@ function copyStartupFiles() {
|
|
|
122
104
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
123
105
|
}
|
|
124
106
|
|
|
125
|
-
|
|
126
|
-
const sourcePath = path.join(dirname, "startup-scripts", file);
|
|
107
|
+
const sourcePath = getStartupScriptSourcePath(import.meta.url, file);
|
|
127
108
|
fs.copyFileSync(sourcePath, targetPath);
|
|
128
109
|
}
|
|
129
110
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
set -
|
|
1
|
+
set -l safe_chain_script (status filename)
|
|
2
|
+
set -l safe_chain_scripts_dir (dirname $safe_chain_script)
|
|
3
|
+
set -l safe_chain_base (dirname $safe_chain_scripts_dir)
|
|
4
|
+
set -gx PATH $PATH $safe_chain_base/bin
|
|
2
5
|
|
|
3
6
|
function npx
|
|
4
7
|
wrapSafeChainCommand "npx" $argv
|
|
@@ -51,6 +54,10 @@ function uv
|
|
|
51
54
|
wrapSafeChainCommand "uv" $argv
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
function uvx
|
|
58
|
+
wrapSafeChainCommand "uvx" $argv
|
|
59
|
+
end
|
|
60
|
+
|
|
54
61
|
function poetry
|
|
55
62
|
wrapSafeChainCommand "poetry" $argv
|
|
56
63
|
end
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
|
2
|
+
_sc_script_path="${BASH_SOURCE[0]}"
|
|
3
|
+
elif [ -n "${ZSH_VERSION:-}" ]; then
|
|
4
|
+
# ${(%):-%x} uses Zsh prompt expansion to get the sourced file's path.
|
|
5
|
+
# eval is required so other shells don't try to parse the Zsh-specific syntax.
|
|
6
|
+
eval '_sc_script_path="${(%):-%x}"'
|
|
7
|
+
else
|
|
8
|
+
_sc_script_path="$0"
|
|
9
|
+
fi
|
|
10
|
+
_sc_scripts_dir=$(CDPATH= cd -- "$(dirname -- "$_sc_script_path")" 2>/dev/null && pwd -P)
|
|
11
|
+
_sc_base=$(dirname -- "$_sc_scripts_dir")
|
|
12
|
+
export PATH="$PATH:${_sc_base}/bin"
|
|
13
|
+
unset _sc_base _sc_script_path _sc_scripts_dir
|
|
2
14
|
|
|
3
15
|
function npx() {
|
|
4
16
|
wrapSafeChainCommand "npx" "$@"
|
|
@@ -47,6 +59,10 @@ function uv() {
|
|
|
47
59
|
wrapSafeChainCommand "uv" "$@"
|
|
48
60
|
}
|
|
49
61
|
|
|
62
|
+
function uvx() {
|
|
63
|
+
wrapSafeChainCommand "uvx" "$@"
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
function poetry() {
|
|
51
67
|
wrapSafeChainCommand "poetry" "$@"
|
|
52
68
|
}
|