@aikidosec/safe-chain 0.0.1-custom-install-dir
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 +537 -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 +17 -0
- package/bin/aikido-pip3.js +17 -0
- package/bin/aikido-pipx.js +16 -0
- package/bin/aikido-pnpm.js +14 -0
- package/bin/aikido-pnpx.js +14 -0
- package/bin/aikido-poetry.js +13 -0
- package/bin/aikido-python.js +19 -0
- package/bin/aikido-python3.js +19 -0
- package/bin/aikido-uv.js +16 -0
- package/bin/aikido-uvx.js +16 -0
- package/bin/aikido-yarn.js +14 -0
- package/bin/safe-chain.js +147 -0
- package/docs/Release.md +25 -0
- package/docs/banner.svg +151 -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/docs/troubleshooting.md +321 -0
- package/npm-shrinkwrap.json +3180 -0
- package/package.json +71 -0
- package/src/api/aikido.js +187 -0
- package/src/api/npmApi.js +71 -0
- package/src/config/cliArguments.js +161 -0
- package/src/config/configFile.js +327 -0
- package/src/config/environmentVariables.js +57 -0
- package/src/config/safeChainDir.js +71 -0
- package/src/config/settings.js +247 -0
- package/src/environment/environment.js +14 -0
- package/src/environment/userInteraction.js +122 -0
- package/src/installLocation.js +42 -0
- package/src/main.js +123 -0
- package/src/packagemanager/_shared/commandErrors.js +17 -0
- package/src/packagemanager/_shared/matchesCommand.js +18 -0
- package/src/packagemanager/bun/createBunPackageManager.js +48 -0
- package/src/packagemanager/currentPackageManager.js +82 -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 +20 -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 +20 -0
- package/src/packagemanager/pip/createPackageManager.js +25 -0
- package/src/packagemanager/pip/pipSettings.js +6 -0
- package/src/packagemanager/pip/runPipCommand.js +209 -0
- package/src/packagemanager/pipx/createPipXPackageManager.js +18 -0
- package/src/packagemanager/pipx/runPipXCommand.js +60 -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 +32 -0
- package/src/packagemanager/poetry/createPoetryPackageManager.js +72 -0
- package/src/packagemanager/uv/createUvPackageManager.js +18 -0
- package/src/packagemanager/uv/runUvCommand.js +66 -0
- package/src/packagemanager/uvx/createUvxPackageManager.js +18 -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 +36 -0
- package/src/registryProxy/certBundle.js +203 -0
- package/src/registryProxy/certUtils.js +178 -0
- package/src/registryProxy/getConnectTimeout.js +13 -0
- package/src/registryProxy/http-utils.js +80 -0
- package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
- package/src/registryProxy/interceptors/interceptorBuilder.js +179 -0
- package/src/registryProxy/interceptors/minimumPackageAgeExclusions.js +33 -0
- package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +180 -0
- package/src/registryProxy/interceptors/npm/npmInterceptor.js +101 -0
- package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +60 -0
- package/src/registryProxy/interceptors/pip/modifyPipInfo.js +167 -0
- package/src/registryProxy/interceptors/pip/modifyPipJsonResponse.js +176 -0
- package/src/registryProxy/interceptors/pip/parsePipPackageUrl.js +162 -0
- package/src/registryProxy/interceptors/pip/pipInterceptor.js +122 -0
- package/src/registryProxy/interceptors/pip/pipMetadataResponseUtils.js +27 -0
- package/src/registryProxy/interceptors/pip/pipMetadataVersionUtils.js +131 -0
- package/src/registryProxy/interceptors/suppressedVersionsState.js +21 -0
- package/src/registryProxy/isImdsEndpoint.js +13 -0
- package/src/registryProxy/mitmRequestHandler.js +240 -0
- package/src/registryProxy/plainHttpProxy.js +95 -0
- package/src/registryProxy/registryProxy.js +255 -0
- package/src/registryProxy/tunnelRequestHandler.js +213 -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/scanning/newPackagesDatabaseBuilder.js +71 -0
- package/src/scanning/newPackagesDatabaseWarnings.js +17 -0
- package/src/scanning/newPackagesListCache.js +126 -0
- package/src/scanning/packageNameVariants.js +29 -0
- package/src/shell-integration/helpers.js +296 -0
- package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +37 -0
- package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +25 -0
- package/src/shell-integration/setup-ci.js +152 -0
- package/src/shell-integration/setup.js +110 -0
- package/src/shell-integration/shellDetection.js +39 -0
- package/src/shell-integration/startup-scripts/init-fish.fish +122 -0
- package/src/shell-integration/startup-scripts/init-posix.sh +112 -0
- package/src/shell-integration/startup-scripts/init-pwsh.ps1 +176 -0
- package/src/shell-integration/supported-shells/bash.js +222 -0
- package/src/shell-integration/supported-shells/fish.js +97 -0
- package/src/shell-integration/supported-shells/powershell.js +102 -0
- package/src/shell-integration/supported-shells/windowsPowershell.js +102 -0
- package/src/shell-integration/supported-shells/zsh.js +94 -0
- package/src/shell-integration/teardown.js +114 -0
- package/src/utils/safeSpawn.js +153 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import {
|
|
3
|
+
fetchNewPackagesList,
|
|
4
|
+
fetchNewPackagesListVersion,
|
|
5
|
+
} from "../api/aikido.js";
|
|
6
|
+
import {
|
|
7
|
+
getNewPackagesListPath,
|
|
8
|
+
getNewPackagesListVersionPath,
|
|
9
|
+
} from "../config/configFile.js";
|
|
10
|
+
import { ui } from "../environment/userInteraction.js";
|
|
11
|
+
import { buildNewPackagesDatabase } from "./newPackagesDatabaseBuilder.js";
|
|
12
|
+
import { warnOnceAboutUnavailableDatabase } from "./newPackagesDatabaseWarnings.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {import("./newPackagesDatabaseBuilder.js").NewPackagesDatabase} NewPackagesDatabase
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Shared per-process cache to avoid rebuilding the same feed-backed database on each request.
|
|
19
|
+
/** @type {NewPackagesDatabase | null} */
|
|
20
|
+
let cachedNewPackagesDatabase = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @returns {Promise<NewPackagesDatabase>}
|
|
24
|
+
*/
|
|
25
|
+
export async function openNewPackagesDatabase() {
|
|
26
|
+
if (cachedNewPackagesDatabase) {
|
|
27
|
+
return cachedNewPackagesDatabase;
|
|
28
|
+
}
|
|
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;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @returns {Promise<import("../api/aikido.js").NewPackageEntry[]>}
|
|
47
|
+
*/
|
|
48
|
+
async function getNewPackagesList() {
|
|
49
|
+
const { newPackagesList: cachedList, version: cachedVersion } =
|
|
50
|
+
readNewPackagesListFromLocalCache();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (cachedList) {
|
|
54
|
+
const currentVersion = await fetchNewPackagesListVersion();
|
|
55
|
+
if (cachedVersion === currentVersion) {
|
|
56
|
+
return cachedList;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { newPackagesList, version } = await fetchNewPackagesList();
|
|
61
|
+
|
|
62
|
+
if (version) {
|
|
63
|
+
writeNewPackagesListToLocalCache(newPackagesList, version);
|
|
64
|
+
return newPackagesList;
|
|
65
|
+
} else {
|
|
66
|
+
ui.writeWarning(
|
|
67
|
+
"The new packages list for direct package download request blocking was downloaded, but could not be cached due to a missing version."
|
|
68
|
+
);
|
|
69
|
+
return newPackagesList;
|
|
70
|
+
}
|
|
71
|
+
} catch (/** @type {any} */ error) {
|
|
72
|
+
if (cachedList) {
|
|
73
|
+
ui.writeWarning(
|
|
74
|
+
"Failed to fetch the latest new packages list for direct package download request blocking. Using cached version."
|
|
75
|
+
);
|
|
76
|
+
return cachedList;
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {import("../api/aikido.js").NewPackageEntry[]} data
|
|
84
|
+
* @param {string | number} version
|
|
85
|
+
*
|
|
86
|
+
* @returns {void}
|
|
87
|
+
*/
|
|
88
|
+
export function writeNewPackagesListToLocalCache(data, version) {
|
|
89
|
+
try {
|
|
90
|
+
const listPath = getNewPackagesListPath();
|
|
91
|
+
const versionPath = getNewPackagesListVersionPath();
|
|
92
|
+
|
|
93
|
+
fs.writeFileSync(listPath, JSON.stringify(data));
|
|
94
|
+
fs.writeFileSync(versionPath, version.toString());
|
|
95
|
+
} catch {
|
|
96
|
+
ui.writeWarning(
|
|
97
|
+
"Failed to write new packages list to local cache, next time the list will be fetched from the server again."
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @returns {{newPackagesList: import("../api/aikido.js").NewPackageEntry[] | null, version: string | null}}
|
|
104
|
+
*/
|
|
105
|
+
export function readNewPackagesListFromLocalCache() {
|
|
106
|
+
try {
|
|
107
|
+
const listPath = getNewPackagesListPath();
|
|
108
|
+
if (!fs.existsSync(listPath)) {
|
|
109
|
+
return { newPackagesList: null, version: null };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const data = fs.readFileSync(listPath, "utf8");
|
|
113
|
+
const newPackagesList = JSON.parse(data);
|
|
114
|
+
const versionPath = getNewPackagesListVersionPath();
|
|
115
|
+
let version = null;
|
|
116
|
+
if (fs.existsSync(versionPath)) {
|
|
117
|
+
version = fs.readFileSync(versionPath, "utf8").trim();
|
|
118
|
+
}
|
|
119
|
+
return { newPackagesList, version };
|
|
120
|
+
} catch {
|
|
121
|
+
ui.writeWarning(
|
|
122
|
+
"Failed to read new packages list from local cache. Continuing without local cache."
|
|
123
|
+
);
|
|
124
|
+
return { newPackagesList: null, version: null };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ECOSYSTEM_PY } from "../config/settings.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalises a Python package name per PEP 503: lowercase and collapse any
|
|
5
|
+
* run of `.`, `_`, or `-` into a single hyphen.
|
|
6
|
+
* @param {string} packageName
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function normalizePipPackageName(packageName) {
|
|
10
|
+
return packageName.toLowerCase().replace(/[._-]+/g, "-");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} packageName
|
|
15
|
+
* @param {string} ecosystem
|
|
16
|
+
* @returns {string[]}
|
|
17
|
+
*/
|
|
18
|
+
export function getEquivalentPackageNames(packageName, ecosystem) {
|
|
19
|
+
if (ecosystem !== ECOSYSTEM_PY) {
|
|
20
|
+
return [packageName];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const pythonSeparatorPattern = /[._-]/g;
|
|
24
|
+
const hyphenName = packageName.replaceAll(pythonSeparatorPattern, "-");
|
|
25
|
+
const underscoreName = packageName.replaceAll(pythonSeparatorPattern, "_");
|
|
26
|
+
const dotName = packageName.replaceAll(pythonSeparatorPattern, ".");
|
|
27
|
+
|
|
28
|
+
return [...new Set([packageName, hyphenName, underscoreName, dotName])];
|
|
29
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
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
|
+
import { safeSpawn } from "../utils/safeSpawn.js";
|
|
7
|
+
import { ui } from "../environment/userInteraction.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} AikidoTool
|
|
11
|
+
* @property {string} tool
|
|
12
|
+
* @property {string} aikidoCommand
|
|
13
|
+
* @property {string} ecoSystem
|
|
14
|
+
* @property {string} internalPackageManagerName
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @type {AikidoTool[]}
|
|
19
|
+
*/
|
|
20
|
+
export const knownAikidoTools = [
|
|
21
|
+
{
|
|
22
|
+
tool: "npm",
|
|
23
|
+
aikidoCommand: "aikido-npm",
|
|
24
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
25
|
+
internalPackageManagerName: "npm",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
tool: "npx",
|
|
29
|
+
aikidoCommand: "aikido-npx",
|
|
30
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
31
|
+
internalPackageManagerName: "npx",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
tool: "yarn",
|
|
35
|
+
aikidoCommand: "aikido-yarn",
|
|
36
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
37
|
+
internalPackageManagerName: "yarn",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
tool: "pnpm",
|
|
41
|
+
aikidoCommand: "aikido-pnpm",
|
|
42
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
43
|
+
internalPackageManagerName: "pnpm",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
tool: "pnpx",
|
|
47
|
+
aikidoCommand: "aikido-pnpx",
|
|
48
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
49
|
+
internalPackageManagerName: "pnpx",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
tool: "bun",
|
|
53
|
+
aikidoCommand: "aikido-bun",
|
|
54
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
55
|
+
internalPackageManagerName: "bun",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
tool: "bunx",
|
|
59
|
+
aikidoCommand: "aikido-bunx",
|
|
60
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
61
|
+
internalPackageManagerName: "bunx",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
tool: "uv",
|
|
65
|
+
aikidoCommand: "aikido-uv",
|
|
66
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
67
|
+
internalPackageManagerName: "uv",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
tool: "uvx",
|
|
71
|
+
aikidoCommand: "aikido-uvx",
|
|
72
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
73
|
+
internalPackageManagerName: "uvx",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
tool: "pip",
|
|
77
|
+
aikidoCommand: "aikido-pip",
|
|
78
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
79
|
+
internalPackageManagerName: "pip",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
tool: "pip3",
|
|
83
|
+
aikidoCommand: "aikido-pip3",
|
|
84
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
85
|
+
internalPackageManagerName: "pip",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
tool: "poetry",
|
|
89
|
+
aikidoCommand: "aikido-poetry",
|
|
90
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
91
|
+
internalPackageManagerName: "poetry",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
tool: "python",
|
|
95
|
+
aikidoCommand: "aikido-python",
|
|
96
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
97
|
+
internalPackageManagerName: "pip",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
tool: "python3",
|
|
101
|
+
aikidoCommand: "aikido-python3",
|
|
102
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
103
|
+
internalPackageManagerName: "pip",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
tool: "pipx",
|
|
107
|
+
aikidoCommand: "aikido-pipx",
|
|
108
|
+
ecoSystem: ECOSYSTEM_PY,
|
|
109
|
+
internalPackageManagerName: "pipx",
|
|
110
|
+
},
|
|
111
|
+
// When adding a new tool here, also update the documentation for the new tool in the README.md
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns a formatted string listing all supported package managers.
|
|
116
|
+
* Example: "npm, npx, yarn, pnpm, and pnpx commands"
|
|
117
|
+
*/
|
|
118
|
+
export function getPackageManagerList() {
|
|
119
|
+
const tools = knownAikidoTools.map((t) => t.tool);
|
|
120
|
+
if (tools.length <= 1) {
|
|
121
|
+
return `${tools[0] || ""} commands`;
|
|
122
|
+
}
|
|
123
|
+
if (tools.length === 2) {
|
|
124
|
+
return `${tools[0]} and ${tools[1]} commands`;
|
|
125
|
+
}
|
|
126
|
+
const lastTool = tools.pop();
|
|
127
|
+
return `${tools.join(", ")}, and ${lastTool} commands`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} executableName
|
|
132
|
+
*
|
|
133
|
+
* @returns {boolean}
|
|
134
|
+
*/
|
|
135
|
+
export function doesExecutableExistOnSystem(executableName) {
|
|
136
|
+
if (os.platform() === "win32") {
|
|
137
|
+
const result = spawnSync("where", [executableName], { stdio: "ignore" });
|
|
138
|
+
return result.status === 0;
|
|
139
|
+
} else {
|
|
140
|
+
const result = spawnSync("which", [executableName], { stdio: "ignore" });
|
|
141
|
+
return result.status === 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} filePath
|
|
147
|
+
* @param {RegExp} pattern
|
|
148
|
+
* @param {string} [eol]
|
|
149
|
+
*
|
|
150
|
+
* @returns {void}
|
|
151
|
+
*/
|
|
152
|
+
export function removeLinesMatchingPattern(filePath, pattern, eol) {
|
|
153
|
+
if (!fs.existsSync(filePath)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
eol = eol || os.EOL;
|
|
158
|
+
|
|
159
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
160
|
+
const lines = fileContent.split(/\r?\n|\r|\u2028|\u2029/);
|
|
161
|
+
const updatedLines = lines.filter((line) => !shouldRemoveLine(line, pattern));
|
|
162
|
+
fs.writeFileSync(filePath, updatedLines.join(eol), "utf-8");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const maxLineLength = 100;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {string} line
|
|
169
|
+
* @param {RegExp} pattern
|
|
170
|
+
* @returns {boolean}
|
|
171
|
+
*/
|
|
172
|
+
function shouldRemoveLine(line, pattern) {
|
|
173
|
+
const isPatternMatch = pattern.test(line);
|
|
174
|
+
|
|
175
|
+
if (!isPatternMatch) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (line.length > maxLineLength) {
|
|
180
|
+
// safe-chain only adds lines shorter than maxLineLength
|
|
181
|
+
// so if the line is longer, it must be from a different
|
|
182
|
+
// source and could be dangerous to remove
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
line.includes("\n") ||
|
|
188
|
+
line.includes("\r") ||
|
|
189
|
+
line.includes("\u2028") ||
|
|
190
|
+
line.includes("\u2029")
|
|
191
|
+
) {
|
|
192
|
+
// If the line contains newlines, something has gone wrong in splitting
|
|
193
|
+
// \u2028 and \u2029 are Unicode line separator characters (line and paragraph separators)
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {string} filePath
|
|
202
|
+
* @param {string} line
|
|
203
|
+
* @param {string} [eol]
|
|
204
|
+
*
|
|
205
|
+
* @returns {void}
|
|
206
|
+
*/
|
|
207
|
+
export function addLineToFile(filePath, line, eol) {
|
|
208
|
+
createFileIfNotExists(filePath);
|
|
209
|
+
|
|
210
|
+
eol = eol || os.EOL;
|
|
211
|
+
|
|
212
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
213
|
+
let updatedContent = fileContent;
|
|
214
|
+
|
|
215
|
+
if (!fileContent.endsWith(eol)) {
|
|
216
|
+
updatedContent += eol;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
updatedContent += line + eol;
|
|
220
|
+
fs.writeFileSync(filePath, updatedContent, "utf-8");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} filePath
|
|
225
|
+
*
|
|
226
|
+
* @returns {void}
|
|
227
|
+
*/
|
|
228
|
+
function createFileIfNotExists(filePath) {
|
|
229
|
+
if (fs.existsSync(filePath)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const dir = path.dirname(filePath);
|
|
234
|
+
if (!fs.existsSync(dir)) {
|
|
235
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fs.writeFileSync(filePath, "", "utf-8");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Checks if PowerShell execution policy allows script execution
|
|
243
|
+
* @param {string} shellExecutableName - The name of the PowerShell executable ("pwsh" or "powershell")
|
|
244
|
+
* @returns {Promise<{isValid: boolean, policy: string}>} validation result
|
|
245
|
+
*/
|
|
246
|
+
export async function validatePowerShellExecutionPolicy(shellExecutableName) {
|
|
247
|
+
// Security: Only allow known shell executables
|
|
248
|
+
const validShells = ["pwsh", "powershell"];
|
|
249
|
+
if (!validShells.includes(shellExecutableName)) {
|
|
250
|
+
return { isValid: false, policy: "Unknown" };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// For Windows PowerShell (5.1), clean PSModulePath to avoid conflicts with PowerShell 7 modules
|
|
255
|
+
// When safe-chain is invoked from PowerShell 7, it sets its module paths to PSModulePath, causing
|
|
256
|
+
// Windows PowerShell to try loading incompatible PowerShell 7 modules.
|
|
257
|
+
// Setting the environment to Windows PowerShell's modules fixes this.
|
|
258
|
+
let spawnOptions;
|
|
259
|
+
if (shellExecutableName === "powershell") {
|
|
260
|
+
const userProfile = process.env.USERPROFILE || "";
|
|
261
|
+
const cleanPSModulePath = [
|
|
262
|
+
path.join(userProfile, "Documents", "WindowsPowerShell", "Modules"),
|
|
263
|
+
"C:\\Program Files\\WindowsPowerShell\\Modules",
|
|
264
|
+
"C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules",
|
|
265
|
+
].join(";");
|
|
266
|
+
|
|
267
|
+
spawnOptions = {
|
|
268
|
+
env: {
|
|
269
|
+
...process.env,
|
|
270
|
+
PSModulePath: cleanPSModulePath,
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
} else {
|
|
274
|
+
spawnOptions = {};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const commandResult = await safeSpawn(
|
|
278
|
+
shellExecutableName,
|
|
279
|
+
["-Command", "Get-ExecutionPolicy"],
|
|
280
|
+
spawnOptions,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const policy = commandResult.stdout.trim();
|
|
284
|
+
|
|
285
|
+
const acceptablePolicies = ["RemoteSigned", "Unrestricted", "Bypass"];
|
|
286
|
+
return {
|
|
287
|
+
isValid: acceptablePolicies.includes(policy),
|
|
288
|
+
policy: policy,
|
|
289
|
+
};
|
|
290
|
+
} catch (err) {
|
|
291
|
+
ui.writeWarning(
|
|
292
|
+
`An error happened while trying to find the current executionpolicy in powershell: ${err}`,
|
|
293
|
+
);
|
|
294
|
+
return { isValid: false, policy: "Unknown" };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
_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"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if command -v safe-chain >/dev/null 2>&1; then
|
|
23
|
+
# Remove shim directory from PATH when calling {{AIKIDO_COMMAND}} to prevent infinite loops
|
|
24
|
+
PATH=$(remove_shim_from_path) exec safe-chain {{PACKAGE_MANAGER}} "$@"
|
|
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
|
+
|
|
29
|
+
# Dynamically find original {{PACKAGE_MANAGER}} (excluding this shim directory)
|
|
30
|
+
original_cmd=$(PATH=$(remove_shim_from_path) command -v {{PACKAGE_MANAGER}})
|
|
31
|
+
if [ -n "$original_cmd" ]; then
|
|
32
|
+
exec "$original_cmd" "$@"
|
|
33
|
+
else
|
|
34
|
+
echo "Error: Could not find original {{PACKAGE_MANAGER}}" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
fi
|
|
@@ -0,0 +1,25 @@
|
|
|
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=%~dp0"
|
|
7
|
+
if "%SHIM_DIR:~-1%"=="\" set "SHIM_DIR=%SHIM_DIR:~0,-1%"
|
|
8
|
+
call set "CLEAN_PATH=%%PATH:%SHIM_DIR%;=%%"
|
|
9
|
+
|
|
10
|
+
REM Check if aikido command is available with clean PATH
|
|
11
|
+
set "PATH=%CLEAN_PATH%" & where safe-chain >nul 2>&1
|
|
12
|
+
if %errorlevel%==0 (
|
|
13
|
+
REM Call aikido command with clean PATH
|
|
14
|
+
set "PATH=%CLEAN_PATH%" & safe-chain {{PACKAGE_MANAGER}} %*
|
|
15
|
+
) else (
|
|
16
|
+
REM Find the original command with clean PATH
|
|
17
|
+
for /f "tokens=*" %%i in ('set "PATH=%CLEAN_PATH%" ^& where {{PACKAGE_MANAGER}} 2^>nul') do (
|
|
18
|
+
"%%i" %*
|
|
19
|
+
goto :eof
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
REM If we get here, original command was not found
|
|
23
|
+
echo Error: Could not find original {{PACKAGE_MANAGER}} >&2
|
|
24
|
+
exit /b 1
|
|
25
|
+
)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ui } from "../environment/userInteraction.js";
|
|
3
|
+
import { getPackageManagerList, knownAikidoTools } from "./helpers.js";
|
|
4
|
+
import {
|
|
5
|
+
getShimsDir,
|
|
6
|
+
getBinDir,
|
|
7
|
+
getPathWrapperTemplatePath,
|
|
8
|
+
} from "../config/safeChainDir.js";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import path from "path";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Loops over the detected shells and calls the setup function for each.
|
|
15
|
+
*/
|
|
16
|
+
export async function setupCi() {
|
|
17
|
+
ui.writeInformation(
|
|
18
|
+
chalk.bold("Setting up shell aliases.") +
|
|
19
|
+
` This will wrap safe-chain around ${getPackageManagerList()}.`
|
|
20
|
+
);
|
|
21
|
+
ui.emptyLine();
|
|
22
|
+
|
|
23
|
+
const shimsDir = getShimsDir();
|
|
24
|
+
const binDir = getBinDir();
|
|
25
|
+
// Create the shims directory if it doesn't exist
|
|
26
|
+
if (!fs.existsSync(shimsDir)) {
|
|
27
|
+
fs.mkdirSync(shimsDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
createShims(shimsDir);
|
|
31
|
+
ui.writeInformation(`Created shims in ${shimsDir}`);
|
|
32
|
+
modifyPathForCi(shimsDir, binDir);
|
|
33
|
+
ui.writeInformation(`Added shims directory to PATH for CI environments.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} shimsDir
|
|
38
|
+
*
|
|
39
|
+
* @returns {void}
|
|
40
|
+
*/
|
|
41
|
+
function createUnixShims(shimsDir) {
|
|
42
|
+
// Read the template file
|
|
43
|
+
const templatePath = getPathWrapperTemplatePath(import.meta.url, "unix-wrapper.template.sh");
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(templatePath)) {
|
|
46
|
+
ui.writeError(`Template file not found: ${templatePath}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const template = fs.readFileSync(templatePath, "utf-8");
|
|
51
|
+
|
|
52
|
+
// Create a shim for each tool
|
|
53
|
+
let created = 0;
|
|
54
|
+
for (const toolInfo of getToolsToSetup()) {
|
|
55
|
+
const shimContent = template
|
|
56
|
+
.replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
|
|
57
|
+
.replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
|
|
58
|
+
|
|
59
|
+
const shimPath = path.join(shimsDir, toolInfo.tool);
|
|
60
|
+
fs.writeFileSync(shimPath, shimContent, "utf-8");
|
|
61
|
+
|
|
62
|
+
// Make the shim executable on Unix systems
|
|
63
|
+
fs.chmodSync(shimPath, 0o755);
|
|
64
|
+
created++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ui.writeInformation(`Created ${created} Unix shim(s) in ${shimsDir}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} shimsDir
|
|
72
|
+
*
|
|
73
|
+
* @returns {void}
|
|
74
|
+
*/
|
|
75
|
+
function createWindowsShims(shimsDir) {
|
|
76
|
+
// Read the template file
|
|
77
|
+
const templatePath = getPathWrapperTemplatePath(import.meta.url, "windows-wrapper.template.cmd");
|
|
78
|
+
|
|
79
|
+
if (!fs.existsSync(templatePath)) {
|
|
80
|
+
ui.writeError(`Windows template file not found: ${templatePath}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const template = fs.readFileSync(templatePath, "utf-8");
|
|
85
|
+
|
|
86
|
+
// Create a shim for each tool
|
|
87
|
+
let created = 0;
|
|
88
|
+
for (const toolInfo of getToolsToSetup()) {
|
|
89
|
+
const shimContent = template
|
|
90
|
+
.replaceAll("{{PACKAGE_MANAGER}}", toolInfo.tool)
|
|
91
|
+
.replaceAll("{{AIKIDO_COMMAND}}", toolInfo.aikidoCommand);
|
|
92
|
+
|
|
93
|
+
const shimPath = `${shimsDir}/${toolInfo.tool}.cmd`;
|
|
94
|
+
fs.writeFileSync(shimPath, shimContent, "utf-8");
|
|
95
|
+
created++;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ui.writeInformation(`Created ${created} Windows shim(s) in ${shimsDir}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} shimsDir
|
|
103
|
+
*
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
function createShims(shimsDir) {
|
|
107
|
+
if (os.platform() === "win32") {
|
|
108
|
+
createWindowsShims(shimsDir);
|
|
109
|
+
} else {
|
|
110
|
+
createUnixShims(shimsDir);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} shimsDir
|
|
116
|
+
* @param {string} binDir
|
|
117
|
+
*
|
|
118
|
+
* @returns {void}
|
|
119
|
+
*/
|
|
120
|
+
function modifyPathForCi(shimsDir, binDir) {
|
|
121
|
+
if (process.env.GITHUB_PATH) {
|
|
122
|
+
// In GitHub Actions, append the shims directory to GITHUB_PATH
|
|
123
|
+
fs.appendFileSync(
|
|
124
|
+
process.env.GITHUB_PATH,
|
|
125
|
+
shimsDir + os.EOL + binDir + os.EOL,
|
|
126
|
+
"utf-8"
|
|
127
|
+
);
|
|
128
|
+
ui.writeInformation(
|
|
129
|
+
`Added shims directory to GITHUB_PATH for GitHub Actions.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (process.env.TF_BUILD) {
|
|
134
|
+
// In Azure Pipelines, prepending the path is done via a logging command:
|
|
135
|
+
// ##vso[task.prependpath]/path/to/add
|
|
136
|
+
// Logging this to stdout will cause the Azure Pipelines agent to pick it up
|
|
137
|
+
ui.writeInformation("##vso[task.prependpath]" + shimsDir);
|
|
138
|
+
ui.writeInformation("##vso[task.prependpath]" + binDir);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (process.env.BASH_ENV) {
|
|
142
|
+
// In CircleCI, persisting PATH across steps is done by appending shell exports
|
|
143
|
+
// to the file referenced by BASH_ENV. CircleCI sources this file for 'run' each step.
|
|
144
|
+
const exportLine = `export PATH="${shimsDir}:${binDir}:$PATH"` + os.EOL;
|
|
145
|
+
fs.appendFileSync(process.env.BASH_ENV, exportLine, "utf-8");
|
|
146
|
+
ui.writeInformation(`Added shims directory to BASH_ENV for CircleCI.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getToolsToSetup() {
|
|
151
|
+
return knownAikidoTools;
|
|
152
|
+
}
|