@aikidosec/safe-chain 0.0.4-connect-timeout-beta
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/LICENSE +674 -0
- package/README.md +257 -0
- package/bin/aikido-bun.js +14 -0
- package/bin/aikido-bunx.js +14 -0
- package/bin/aikido-npm.js +14 -0
- package/bin/aikido-npx.js +14 -0
- package/bin/aikido-pip.js +20 -0
- package/bin/aikido-pip3.js +21 -0
- package/bin/aikido-pnpm.js +14 -0
- package/bin/aikido-pnpx.js +14 -0
- package/bin/aikido-python.js +30 -0
- package/bin/aikido-python3.js +30 -0
- package/bin/aikido-uv.js +16 -0
- package/bin/aikido-yarn.js +14 -0
- package/bin/safe-chain.js +190 -0
- package/docs/banner.svg +151 -0
- package/docs/npm-to-binary-migration.md +89 -0
- package/docs/safe-package-manager-demo.gif +0 -0
- package/docs/safe-package-manager-demo.png +0 -0
- package/docs/shell-integration.md +149 -0
- package/package.json +68 -0
- package/src/api/aikido.js +54 -0
- package/src/api/npmApi.js +71 -0
- package/src/config/cliArguments.js +138 -0
- package/src/config/configFile.js +192 -0
- package/src/config/environmentVariables.js +7 -0
- package/src/config/settings.js +100 -0
- package/src/environment/environment.js +14 -0
- package/src/environment/userInteraction.js +122 -0
- package/src/main.js +104 -0
- package/src/packagemanager/_shared/matchesCommand.js +18 -0
- package/src/packagemanager/bun/createBunPackageManager.js +53 -0
- package/src/packagemanager/currentPackageManager.js +72 -0
- package/src/packagemanager/npm/createPackageManager.js +72 -0
- package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
- package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
- package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
- package/src/packagemanager/npm/runNpmCommand.js +25 -0
- package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
- package/src/packagemanager/npm/utils/cmd-list.js +174 -0
- package/src/packagemanager/npm/utils/npmCommands.js +34 -0
- package/src/packagemanager/npx/createPackageManager.js +15 -0
- package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
- package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
- package/src/packagemanager/npx/runNpxCommand.js +25 -0
- package/src/packagemanager/pip/createPackageManager.js +21 -0
- package/src/packagemanager/pip/pipSettings.js +30 -0
- package/src/packagemanager/pip/runPipCommand.js +175 -0
- package/src/packagemanager/pnpm/createPackageManager.js +57 -0
- package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
- package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
- package/src/packagemanager/pnpm/runPnpmCommand.js +36 -0
- package/src/packagemanager/uv/createUvPackageManager.js +18 -0
- package/src/packagemanager/uv/runUvCommand.js +71 -0
- package/src/packagemanager/yarn/createPackageManager.js +41 -0
- package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
- package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
- package/src/packagemanager/yarn/runYarnCommand.js +41 -0
- package/src/registryProxy/certBundle.js +95 -0
- package/src/registryProxy/certUtils.js +128 -0
- package/src/registryProxy/http-utils.js +17 -0
- package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +140 -0
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +177 -0
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -0
- package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +43 -0
- package/src/registryProxy/interceptors/pipInterceptor.js +115 -0
- package/src/registryProxy/mitmRequestHandler.js +231 -0
- package/src/registryProxy/plainHttpProxy.js +95 -0
- package/src/registryProxy/registryProxy.js +184 -0
- package/src/registryProxy/tunnelRequestHandler.js +180 -0
- package/src/scanning/audit/index.js +129 -0
- package/src/scanning/index.js +82 -0
- package/src/scanning/malwareDatabase.js +131 -0
- package/src/shell-integration/helpers.js +213 -0
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
- package/src/shell-integration/setup-ci.js +170 -0
- package/src/shell-integration/setup.js +127 -0
- package/src/shell-integration/shellDetection.js +37 -0
- package/src/shell-integration/startup-scripts/include-python/init-fish.fish +94 -0
- package/src/shell-integration/startup-scripts/include-python/init-posix.sh +81 -0
- package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +115 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +71 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +58 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +92 -0
- package/src/shell-integration/supported-shells/bash.js +134 -0
- package/src/shell-integration/supported-shells/fish.js +77 -0
- package/src/shell-integration/supported-shells/powershell.js +73 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +73 -0
- package/src/shell-integration/supported-shells/zsh.js +74 -0
- package/src/shell-integration/teardown.js +64 -0
- package/src/utils/safeSpawn.js +137 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ui } from "../../environment/userInteraction.js";
|
|
2
|
+
import {
|
|
3
|
+
MALWARE_STATUS_MALWARE,
|
|
4
|
+
openMalwareDatabase,
|
|
5
|
+
} from "../malwareDatabase.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} PackageChange
|
|
9
|
+
* @property {string} name
|
|
10
|
+
* @property {string} version
|
|
11
|
+
* @property {string} type
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} AuditResult
|
|
16
|
+
* @property {PackageChange[]} allowedChanges
|
|
17
|
+
* @property {(PackageChange & {reason: string})[]} disallowedChanges
|
|
18
|
+
* @property {boolean} isAllowed
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} AuditStats
|
|
23
|
+
* @property {number} totalPackages
|
|
24
|
+
* @property {number} safePackages
|
|
25
|
+
* @property {number} malwarePackages
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @type AuditStats
|
|
30
|
+
*/
|
|
31
|
+
const auditStats = {
|
|
32
|
+
totalPackages: 0,
|
|
33
|
+
safePackages: 0,
|
|
34
|
+
malwarePackages: 0,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @returns {AuditStats}
|
|
39
|
+
*/
|
|
40
|
+
export function getAuditStats() {
|
|
41
|
+
return auditStats;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
*
|
|
46
|
+
* @param {string | undefined} name
|
|
47
|
+
* @param {string | undefined} version
|
|
48
|
+
* @returns {Promise<boolean>}
|
|
49
|
+
*/
|
|
50
|
+
export async function isMalwarePackage(name, version) {
|
|
51
|
+
if (!name || !version) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const auditResult = await auditChanges([{ name, version, type: "add" }]);
|
|
56
|
+
|
|
57
|
+
return !auditResult.isAllowed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {PackageChange[]} changes
|
|
62
|
+
*
|
|
63
|
+
* @returns {Promise<AuditResult>}
|
|
64
|
+
*/
|
|
65
|
+
export async function auditChanges(changes) {
|
|
66
|
+
const allowedChanges = [];
|
|
67
|
+
const disallowedChanges = [];
|
|
68
|
+
|
|
69
|
+
var malwarePackages = await getPackagesWithMalware(
|
|
70
|
+
changes.filter(
|
|
71
|
+
(change) => change.type === "add" || change.type === "change"
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
for (const change of changes) {
|
|
76
|
+
const malwarePackage = malwarePackages.find(
|
|
77
|
+
(pkg) => pkg.name === change.name && pkg.version === change.version
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (malwarePackage) {
|
|
81
|
+
auditStats.malwarePackages += 1;
|
|
82
|
+
ui.writeVerbose(
|
|
83
|
+
`Safe-chain: Package ${change.name}@${change.version} is marked as malware: ${malwarePackage.status}`
|
|
84
|
+
);
|
|
85
|
+
disallowedChanges.push({ ...change, reason: malwarePackage.status });
|
|
86
|
+
} else {
|
|
87
|
+
auditStats.safePackages += 1;
|
|
88
|
+
ui.writeVerbose(
|
|
89
|
+
`Safe-chain: Package ${change.name}@${change.version} is clean`
|
|
90
|
+
);
|
|
91
|
+
allowedChanges.push(change);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
auditStats.totalPackages += 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const auditResults = {
|
|
98
|
+
allowedChanges,
|
|
99
|
+
disallowedChanges,
|
|
100
|
+
isAllowed: disallowedChanges.length === 0,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return auditResults;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {{name: string, version: string, type: string}[]} changes
|
|
108
|
+
* @returns {Promise<{name: string, version: string, status: string}[]>}
|
|
109
|
+
*/
|
|
110
|
+
async function getPackagesWithMalware(changes) {
|
|
111
|
+
if (changes.length === 0) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const malwareDb = await openMalwareDatabase();
|
|
116
|
+
let allVulnerablePackages = [];
|
|
117
|
+
|
|
118
|
+
for (const change of changes) {
|
|
119
|
+
if (malwareDb.isMalware(change.name, change.version)) {
|
|
120
|
+
allVulnerablePackages.push({
|
|
121
|
+
name: change.name,
|
|
122
|
+
version: change.version,
|
|
123
|
+
status: MALWARE_STATUS_MALWARE,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return allVulnerablePackages;
|
|
129
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { auditChanges } from "./audit/index.js";
|
|
2
|
+
import { getScanTimeout } from "../config/configFile.js";
|
|
3
|
+
import { setTimeout } from "timers/promises";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { getPackageManager } from "../packagemanager/currentPackageManager.js";
|
|
6
|
+
import { ui } from "../environment/userInteraction.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string[]} args
|
|
10
|
+
*
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function shouldScanCommand(args) {
|
|
14
|
+
if (!args || args.length === 0) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return getPackageManager().isSupportedCommand(args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string[]} args
|
|
23
|
+
*
|
|
24
|
+
* @returns {Promise<number>}
|
|
25
|
+
*/
|
|
26
|
+
export async function scanCommand(args) {
|
|
27
|
+
if (!shouldScanCommand(args)) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let timedOut = false;
|
|
32
|
+
/** @type {import("./audit/index.js").AuditResult | undefined} */
|
|
33
|
+
let audit;
|
|
34
|
+
|
|
35
|
+
await Promise.race([
|
|
36
|
+
(async () => {
|
|
37
|
+
const packageManager = getPackageManager();
|
|
38
|
+
const changes = await packageManager.getDependencyUpdatesForCommand(args);
|
|
39
|
+
|
|
40
|
+
if (timedOut) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
audit = await auditChanges(changes);
|
|
45
|
+
})(),
|
|
46
|
+
setTimeout(getScanTimeout()).then(() => {
|
|
47
|
+
timedOut = true;
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
if (timedOut) {
|
|
52
|
+
throw new Error("Timeout exceeded while scanning npm install command.");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!audit || audit.isAllowed) {
|
|
56
|
+
return 0;
|
|
57
|
+
} else {
|
|
58
|
+
printMaliciousChanges(audit.disallowedChanges);
|
|
59
|
+
onMalwareFound();
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {import("./audit/index.js").PackageChange[]} changes
|
|
66
|
+
* @return {void}
|
|
67
|
+
*/
|
|
68
|
+
function printMaliciousChanges(changes) {
|
|
69
|
+
ui.writeInformation(
|
|
70
|
+
chalk.red("✖") + " Safe-chain: " + chalk.bold("Malicious changes detected:")
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
for (const change of changes) {
|
|
74
|
+
ui.writeInformation(` - ${change.name}@${change.version}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onMalwareFound() {
|
|
79
|
+
ui.emptyLine();
|
|
80
|
+
ui.writeExitWithoutInstallingMaliciousPackages();
|
|
81
|
+
ui.emptyLine();
|
|
82
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchMalwareDatabase,
|
|
3
|
+
fetchMalwareDatabaseVersion,
|
|
4
|
+
} from "../api/aikido.js";
|
|
5
|
+
import {
|
|
6
|
+
readDatabaseFromLocalCache,
|
|
7
|
+
writeDatabaseToLocalCache,
|
|
8
|
+
} from "../config/configFile.js";
|
|
9
|
+
import { ui } from "../environment/userInteraction.js";
|
|
10
|
+
import { getEcoSystem, ECOSYSTEM_PY } from "../config/settings.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} MalwareDatabase
|
|
14
|
+
* @property {function(string, string): string} getPackageStatus
|
|
15
|
+
* @property {function(string, string): boolean} isMalware
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** @type {MalwareDatabase | null} */
|
|
19
|
+
let cachedMalwareDatabase = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize package name for comparison.
|
|
23
|
+
* For Python packages (PEP-503): lowercase and replace _, -, . with -
|
|
24
|
+
* For js packages: keep as-is (case-sensitive)
|
|
25
|
+
* @param {string} name
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function normalizePackageName(name) {
|
|
29
|
+
const ecosystem = getEcoSystem();
|
|
30
|
+
if (ecosystem === ECOSYSTEM_PY) {
|
|
31
|
+
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return name;
|
|
35
|
+
}
|
|
36
|
+
|
|
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 === "*");
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (!packageData) {
|
|
60
|
+
return MALWARE_STATUS_OK;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return packageData.reason;
|
|
64
|
+
}
|
|
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;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @returns {Promise<import("../api/aikido.js").MalwarePackage[]>}
|
|
80
|
+
*/
|
|
81
|
+
async function getMalwareDatabase() {
|
|
82
|
+
const { malwareDatabase: cachedDatabase, version: cachedVersion } =
|
|
83
|
+
readDatabaseFromLocalCache();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
if (cachedDatabase) {
|
|
87
|
+
const currentVersion = await fetchMalwareDatabaseVersion();
|
|
88
|
+
if (cachedVersion === currentVersion) {
|
|
89
|
+
return cachedDatabase;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { malwareDatabase, version } = await fetchMalwareDatabase();
|
|
94
|
+
|
|
95
|
+
if (version) {
|
|
96
|
+
// Only cache the malware database when we have a version.
|
|
97
|
+
writeDatabaseToLocalCache(malwareDatabase, version);
|
|
98
|
+
return malwareDatabase;
|
|
99
|
+
} else {
|
|
100
|
+
// We received a valid malware database, but the response
|
|
101
|
+
// did not contain an etag header with the version
|
|
102
|
+
ui.writeWarning(
|
|
103
|
+
"The malware database was downloaded, but could not be cached due to a missing version."
|
|
104
|
+
);
|
|
105
|
+
return malwareDatabase;
|
|
106
|
+
}
|
|
107
|
+
} catch (/** @type any */ error) {
|
|
108
|
+
if (cachedDatabase) {
|
|
109
|
+
ui.writeWarning(
|
|
110
|
+
"Failed to fetch the latest malware database. Using cached version."
|
|
111
|
+
);
|
|
112
|
+
return cachedDatabase;
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} status
|
|
120
|
+
*
|
|
121
|
+
* @returns {boolean}
|
|
122
|
+
*/
|
|
123
|
+
function isMalwareStatus(status) {
|
|
124
|
+
let malwareStatus = status.toUpperCase();
|
|
125
|
+
return malwareStatus === MALWARE_STATUS_MALWARE;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const MALWARE_STATUS_OK = "OK";
|
|
129
|
+
export const MALWARE_STATUS_MALWARE = "MALWARE";
|
|
130
|
+
export const MALWARE_STATUS_TELEMETRY = "TELEMETRY";
|
|
131
|
+
export const MALWARE_STATUS_PROTESTWARE = "PROTESTWARE";
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} AikidoTool
|
|
9
|
+
* @property {string} tool
|
|
10
|
+
* @property {string} aikidoCommand
|
|
11
|
+
* @property {string} ecoSystem
|
|
12
|
+
* @property {string} internalPackageManagerName
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @type {AikidoTool[]}
|
|
17
|
+
*/
|
|
18
|
+
export const knownAikidoTools = [
|
|
19
|
+
{
|
|
20
|
+
tool: "npm",
|
|
21
|
+
aikidoCommand: "aikido-npm",
|
|
22
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
23
|
+
internalPackageManagerName: "npm",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
tool: "npx",
|
|
27
|
+
aikidoCommand: "aikido-npx",
|
|
28
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
29
|
+
internalPackageManagerName: "npx",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
tool: "yarn",
|
|
33
|
+
aikidoCommand: "aikido-yarn",
|
|
34
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
35
|
+
internalPackageManagerName: "yarn",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
tool: "pnpm",
|
|
39
|
+
aikidoCommand: "aikido-pnpm",
|
|
40
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
41
|
+
internalPackageManagerName: "pnpm",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
tool: "pnpx",
|
|
45
|
+
aikidoCommand: "aikido-pnpx",
|
|
46
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
47
|
+
internalPackageManagerName: "pnpx",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
tool: "bun",
|
|
51
|
+
aikidoCommand: "aikido-bun",
|
|
52
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
53
|
+
internalPackageManagerName: "bun",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
tool: "bunx",
|
|
57
|
+
aikidoCommand: "aikido-bunx",
|
|
58
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
59
|
+
internalPackageManagerName: "bunx",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
tool: "uv",
|
|
63
|
+
aikidoCommand: "aikido-uv",
|
|
64
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
65
|
+
internalPackageManagerName: "uv",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
tool: "pip",
|
|
69
|
+
aikidoCommand: "aikido-pip",
|
|
70
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
71
|
+
internalPackageManagerName: "pip",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
tool: "pip3",
|
|
75
|
+
aikidoCommand: "aikido-pip3",
|
|
76
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
77
|
+
internalPackageManagerName: "pip",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
tool: "python",
|
|
81
|
+
aikidoCommand: "aikido-python",
|
|
82
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
83
|
+
internalPackageManagerName: "pip",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
tool: "python3",
|
|
87
|
+
aikidoCommand: "aikido-python3",
|
|
88
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
89
|
+
internalPackageManagerName: "pip",
|
|
90
|
+
},
|
|
91
|
+
// When adding a new tool here, also update the documentation for the new tool in the README.md
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns a formatted string listing all supported package managers.
|
|
96
|
+
* Example: "npm, npx, yarn, pnpm, and pnpx commands"
|
|
97
|
+
*/
|
|
98
|
+
export function getPackageManagerList() {
|
|
99
|
+
const tools = knownAikidoTools.map((t) => t.tool);
|
|
100
|
+
if (tools.length <= 1) {
|
|
101
|
+
return `${tools[0] || ""} commands`;
|
|
102
|
+
}
|
|
103
|
+
if (tools.length === 2) {
|
|
104
|
+
return `${tools[0]} and ${tools[1]} commands`;
|
|
105
|
+
}
|
|
106
|
+
const lastTool = tools.pop();
|
|
107
|
+
return `${tools.join(", ")}, and ${lastTool} commands`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} executableName
|
|
112
|
+
*
|
|
113
|
+
* @returns {boolean}
|
|
114
|
+
*/
|
|
115
|
+
export function doesExecutableExistOnSystem(executableName) {
|
|
116
|
+
if (os.platform() === "win32") {
|
|
117
|
+
const result = spawnSync("where", [executableName], { stdio: "ignore" });
|
|
118
|
+
return result.status === 0;
|
|
119
|
+
} else {
|
|
120
|
+
const result = spawnSync("which", [executableName], { stdio: "ignore" });
|
|
121
|
+
return result.status === 0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} filePath
|
|
127
|
+
* @param {RegExp} pattern
|
|
128
|
+
* @param {string} [eol]
|
|
129
|
+
*
|
|
130
|
+
* @returns {void}
|
|
131
|
+
*/
|
|
132
|
+
export function removeLinesMatchingPattern(filePath, pattern, eol) {
|
|
133
|
+
if (!fs.existsSync(filePath)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
eol = eol || os.EOL;
|
|
138
|
+
|
|
139
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
140
|
+
const lines = fileContent.split(/\r?\n|\r|\u2028|\u2029/);
|
|
141
|
+
const updatedLines = lines.filter((line) => !shouldRemoveLine(line, pattern));
|
|
142
|
+
fs.writeFileSync(filePath, updatedLines.join(eol), "utf-8");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const maxLineLength = 100;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} line
|
|
149
|
+
* @param {RegExp} pattern
|
|
150
|
+
* @returns {boolean}
|
|
151
|
+
*/
|
|
152
|
+
function shouldRemoveLine(line, pattern) {
|
|
153
|
+
const isPatternMatch = pattern.test(line);
|
|
154
|
+
|
|
155
|
+
if (!isPatternMatch) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (line.length > maxLineLength) {
|
|
160
|
+
// safe-chain only adds lines shorter than maxLineLength
|
|
161
|
+
// so if the line is longer, it must be from a different
|
|
162
|
+
// source and could be dangerous to remove
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
line.includes("\n") ||
|
|
168
|
+
line.includes("\r") ||
|
|
169
|
+
line.includes("\u2028") ||
|
|
170
|
+
line.includes("\u2029")
|
|
171
|
+
) {
|
|
172
|
+
// If the line contains newlines, something has gone wrong in splitting
|
|
173
|
+
// \u2028 and \u2029 are Unicode line separator characters (line and paragraph separators)
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} filePath
|
|
182
|
+
* @param {string} line
|
|
183
|
+
* @param {string} [eol]
|
|
184
|
+
*
|
|
185
|
+
* @returns {void}
|
|
186
|
+
*/
|
|
187
|
+
export function addLineToFile(filePath, line, eol) {
|
|
188
|
+
createFileIfNotExists(filePath);
|
|
189
|
+
|
|
190
|
+
eol = eol || os.EOL;
|
|
191
|
+
|
|
192
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
193
|
+
const updatedContent = fileContent + eol + line + eol;
|
|
194
|
+
fs.writeFileSync(filePath, updatedContent, "utf-8");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {string} filePath
|
|
199
|
+
*
|
|
200
|
+
* @returns {void}
|
|
201
|
+
*/
|
|
202
|
+
function createFileIfNotExists(filePath) {
|
|
203
|
+
if (fs.existsSync(filePath)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const dir = path.dirname(filePath);
|
|
208
|
+
if (!fs.existsSync(dir)) {
|
|
209
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fs.writeFileSync(filePath, "", "utf-8");
|
|
213
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Generated wrapper for {{PACKAGE_MANAGER}} by safe-chain
|
|
3
|
+
# This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments
|
|
4
|
+
|
|
5
|
+
# Function to remove shim from PATH (POSIX-compliant)
|
|
6
|
+
remove_shim_from_path() {
|
|
7
|
+
echo "$PATH" | sed "s|$HOME/.safe-chain/shims:||g"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if command -v safe-chain >/dev/null 2>&1; then
|
|
11
|
+
# Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops
|
|
12
|
+
PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
|
|
13
|
+
else
|
|
14
|
+
# Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory)
|
|
15
|
+
original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}})
|
|
16
|
+
if [ -n "$original_cmd" ]; then
|
|
17
|
+
exec "$original_cmd" "$@"
|
|
18
|
+
else
|
|
19
|
+
echo "Error: Could not find original {{PACKAGE_MANAGER}}" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
fi
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM Generated wrapper for {{PACKAGE_MANAGER}} by safe-chain
|
|
3
|
+
REM This wrapper intercepts {{PACKAGE_MANAGER}} calls for non-interactive environments
|
|
4
|
+
|
|
5
|
+
REM Remove shim directory from PATH to prevent infinite loops
|
|
6
|
+
set "SHIM_DIR=%USERPROFILE%\.safe-chain\shims"
|
|
7
|
+
call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
|
|
8
|
+
|
|
9
|
+
REM Check if aikido command is available with clean PATH
|
|
10
|
+
set "PATH=%CLEAN_PATH%" & where safe-chain >nul 2>&1
|
|
11
|
+
if %errorlevel%==0 (
|
|
12
|
+
REM Call aikido command with clean PATH
|
|
13
|
+
set "PATH=%CLEAN_PATH%" & safe-chain {{PACKAGE_MANAGER}} %*
|
|
14
|
+
) else (
|
|
15
|
+
REM Find the original command with clean PATH
|
|
16
|
+
for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where {{PACKAGE_MANAGER}} 2^>nul') do (
|
|
17
|
+
"%%i" %*
|
|
18
|
+
goto :eof
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
REM If we get here, original command was not found
|
|
22
|
+
echo Error: Could not find original {{PACKAGE_MANAGER}} >&2
|
|
23
|
+
exit /b 1
|
|
24
|
+
)
|