@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,54 @@
|
|
|
1
|
+
import fetch from "make-fetch-happen";
|
|
2
|
+
import { getEcoSystem, ECOSYSTEM_JS, ECOSYSTEM_PY } from "../config/settings.js";
|
|
3
|
+
|
|
4
|
+
const malwareDatabaseUrls = {
|
|
5
|
+
[ECOSYSTEM_JS]: "https://malware-list.aikido.dev/malware_predictions.json",
|
|
6
|
+
[ECOSYSTEM_PY]: "https://malware-list.aikido.dev/malware_pypi.json",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} MalwarePackage
|
|
11
|
+
* @property {string} package_name
|
|
12
|
+
* @property {string} version
|
|
13
|
+
* @property {string} reason
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>}
|
|
18
|
+
*/
|
|
19
|
+
export async function fetchMalwareDatabase() {
|
|
20
|
+
const ecosystem = getEcoSystem();
|
|
21
|
+
const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)];
|
|
22
|
+
const response = await fetch(malwareDatabaseUrl);
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`Error fetching ${ecosystem} malware database: ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
let malwareDatabase = await response.json();
|
|
29
|
+
return {
|
|
30
|
+
malwareDatabase: malwareDatabase,
|
|
31
|
+
version: response.headers.get("etag") || undefined,
|
|
32
|
+
};
|
|
33
|
+
} catch (/** @type {any} */ error) {
|
|
34
|
+
throw new Error(`Error parsing malware database: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @returns {Promise<string | undefined>}
|
|
40
|
+
*/
|
|
41
|
+
export async function fetchMalwareDatabaseVersion() {
|
|
42
|
+
const ecosystem = getEcoSystem();
|
|
43
|
+
const malwareDatabaseUrl = malwareDatabaseUrls[/** @type {keyof typeof malwareDatabaseUrls} */ (ecosystem)];
|
|
44
|
+
const response = await fetch(malwareDatabaseUrl, {
|
|
45
|
+
method: "HEAD",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Error fetching ${ecosystem} malware database version: ${response.statusText}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return response.headers.get("etag") || undefined;
|
|
54
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as semver from "semver";
|
|
2
|
+
import * as npmFetch from "npm-registry-fetch";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} packageName
|
|
6
|
+
* @param {string | null} [versionRange]
|
|
7
|
+
* @returns {Promise<string | null>}
|
|
8
|
+
*/
|
|
9
|
+
export async function resolvePackageVersion(packageName, versionRange) {
|
|
10
|
+
if (!versionRange) {
|
|
11
|
+
versionRange = "latest";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (semver.valid(versionRange)) {
|
|
15
|
+
// The version is a fixed version, no need to resolve
|
|
16
|
+
return versionRange;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const packageInfo = (
|
|
20
|
+
/** @type {{"dist-tags"?: Record<string, string>, versions?: Record<string, unknown>} | null} */
|
|
21
|
+
await getPackageInfo(packageName)
|
|
22
|
+
);
|
|
23
|
+
if (!packageInfo) {
|
|
24
|
+
// It is possible that no version is found (could be a private package, or a package that doesn't exist)
|
|
25
|
+
// In this case, we return null to indicate that we couldn't resolve the version
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const distTags = packageInfo["dist-tags"];
|
|
30
|
+
if (distTags && isDistTags(distTags) && distTags[versionRange]) {
|
|
31
|
+
// If the version range is a dist-tag, return the version associated with that tag
|
|
32
|
+
// e.g., "latest", "next", etc.
|
|
33
|
+
return distTags[versionRange];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!packageInfo.versions) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// If the version range is not a dist-tag, we need to resolve the highest version matching the range.
|
|
41
|
+
// This is useful for ranges like "^1.0.0" or "~2.3.4".
|
|
42
|
+
const availableVersions = Object.keys(packageInfo.versions);
|
|
43
|
+
const resolvedVersion = semver.maxSatisfying(availableVersions, versionRange);
|
|
44
|
+
if (resolvedVersion) {
|
|
45
|
+
return resolvedVersion;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Nothing matched the range, return null
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
*
|
|
54
|
+
* @param {unknown} distTags
|
|
55
|
+
* @returns {distTags is Record<string, string>}
|
|
56
|
+
*/
|
|
57
|
+
function isDistTags(distTags) {
|
|
58
|
+
return typeof distTags === "object";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} packageName
|
|
63
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
64
|
+
*/
|
|
65
|
+
async function getPackageInfo(packageName) {
|
|
66
|
+
try {
|
|
67
|
+
return await npmFetch.json(packageName);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, includePython: boolean}}
|
|
3
|
+
*/
|
|
4
|
+
const state = {
|
|
5
|
+
loggingLevel: undefined,
|
|
6
|
+
skipMinimumPackageAge: undefined,
|
|
7
|
+
minimumPackageAgeHours: undefined,
|
|
8
|
+
includePython: false,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string[]} args
|
|
15
|
+
* @returns {string[]}
|
|
16
|
+
*/
|
|
17
|
+
export function initializeCliArguments(args) {
|
|
18
|
+
// Reset state on each call
|
|
19
|
+
state.loggingLevel = undefined;
|
|
20
|
+
state.skipMinimumPackageAge = undefined;
|
|
21
|
+
state.minimumPackageAgeHours = undefined;
|
|
22
|
+
|
|
23
|
+
const safeChainArgs = [];
|
|
24
|
+
const remainingArgs = [];
|
|
25
|
+
|
|
26
|
+
for (const arg of args) {
|
|
27
|
+
if (arg.toLowerCase().startsWith(SAFE_CHAIN_ARG_PREFIX)) {
|
|
28
|
+
safeChainArgs.push(arg);
|
|
29
|
+
} else {
|
|
30
|
+
remainingArgs.push(arg);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setLoggingLevel(safeChainArgs);
|
|
35
|
+
setSkipMinimumPackageAge(safeChainArgs);
|
|
36
|
+
setMinimumPackageAgeHours(safeChainArgs);
|
|
37
|
+
setIncludePython(args);
|
|
38
|
+
|
|
39
|
+
return remainingArgs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string[]} args
|
|
44
|
+
* @param {string} prefix
|
|
45
|
+
* @returns {string | undefined}
|
|
46
|
+
*/
|
|
47
|
+
function getLastArgEqualsValue(args, prefix) {
|
|
48
|
+
for (var i = args.length - 1; i >= 0; i--) {
|
|
49
|
+
const arg = args[i];
|
|
50
|
+
if (arg.toLowerCase().startsWith(prefix)) {
|
|
51
|
+
return arg.substring(prefix.length);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string[]} args
|
|
60
|
+
* @returns {void}
|
|
61
|
+
*/
|
|
62
|
+
function setLoggingLevel(args) {
|
|
63
|
+
const safeChainLoggingArg = SAFE_CHAIN_ARG_PREFIX + "logging=";
|
|
64
|
+
|
|
65
|
+
const level = getLastArgEqualsValue(args, safeChainLoggingArg);
|
|
66
|
+
if (!level) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
state.loggingLevel = level.toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getLoggingLevel() {
|
|
73
|
+
return state.loggingLevel;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string[]} args
|
|
78
|
+
* @returns {void}
|
|
79
|
+
*/
|
|
80
|
+
function setSkipMinimumPackageAge(args) {
|
|
81
|
+
const flagName = SAFE_CHAIN_ARG_PREFIX + "skip-minimum-package-age";
|
|
82
|
+
|
|
83
|
+
if (hasFlagArg(args, flagName)) {
|
|
84
|
+
state.skipMinimumPackageAge = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getSkipMinimumPackageAge() {
|
|
89
|
+
return state.skipMinimumPackageAge;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string[]} args
|
|
94
|
+
* @returns {void}
|
|
95
|
+
*/
|
|
96
|
+
function setMinimumPackageAgeHours(args) {
|
|
97
|
+
const argName = SAFE_CHAIN_ARG_PREFIX + "minimum-package-age-hours=";
|
|
98
|
+
|
|
99
|
+
const value = getLastArgEqualsValue(args, argName);
|
|
100
|
+
if (value) {
|
|
101
|
+
state.minimumPackageAgeHours = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @returns {string | undefined}
|
|
107
|
+
*/
|
|
108
|
+
export function getMinimumPackageAgeHours() {
|
|
109
|
+
return state.minimumPackageAgeHours;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {string[]} args
|
|
114
|
+
*/
|
|
115
|
+
function setIncludePython(args) {
|
|
116
|
+
// This flag doesn't have the --safe-chain- prefix because
|
|
117
|
+
// it is only used for the safe-chain command itself and
|
|
118
|
+
// not when wrapped around package manager commands.
|
|
119
|
+
state.includePython = hasFlagArg(args, "--include-python");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function includePython() {
|
|
123
|
+
return state.includePython;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {string[]} args
|
|
128
|
+
* @param {string} flagName
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
function hasFlagArg(args, flagName) {
|
|
132
|
+
for (const arg of args) {
|
|
133
|
+
if (arg.toLowerCase() === flagName.toLowerCase()) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { ui } from "../environment/userInteraction.js";
|
|
5
|
+
import { getEcoSystem } from "./settings.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} SafeChainConfig
|
|
9
|
+
*
|
|
10
|
+
* This should be a number, but can be anything because it is user-input.
|
|
11
|
+
* We cannot trust the input and should add the necessary validations.
|
|
12
|
+
* @property {unknown} scanTimeout
|
|
13
|
+
* @property {unknown} minimumPackageAgeHours
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @returns {number}
|
|
18
|
+
*/
|
|
19
|
+
export function getScanTimeout() {
|
|
20
|
+
const config = readConfigFile();
|
|
21
|
+
|
|
22
|
+
if (process.env.AIKIDO_SCAN_TIMEOUT_MS) {
|
|
23
|
+
const scanTimeout = validateTimeout(process.env.AIKIDO_SCAN_TIMEOUT_MS);
|
|
24
|
+
if (scanTimeout != null) {
|
|
25
|
+
return scanTimeout;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (config.scanTimeout) {
|
|
30
|
+
const scanTimeout = validateTimeout(config.scanTimeout);
|
|
31
|
+
if (scanTimeout != null) {
|
|
32
|
+
return scanTimeout;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return 10000; // Default to 10 seconds
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
*
|
|
41
|
+
* @param {any} value
|
|
42
|
+
* @returns {number?}
|
|
43
|
+
*/
|
|
44
|
+
function validateTimeout(value) {
|
|
45
|
+
const timeout = Number(value);
|
|
46
|
+
if (!Number.isNaN(timeout) && timeout > 0) {
|
|
47
|
+
return timeout;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {any} value
|
|
54
|
+
* @returns {number | undefined}
|
|
55
|
+
*/
|
|
56
|
+
function validateMinimumPackageAgeHours(value) {
|
|
57
|
+
const hours = Number(value);
|
|
58
|
+
if (!Number.isNaN(hours)) {
|
|
59
|
+
return hours;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Gets the minimum package age in hours from config file only
|
|
66
|
+
* @returns {number | undefined}
|
|
67
|
+
*/
|
|
68
|
+
export function getMinimumPackageAgeHours() {
|
|
69
|
+
const config = readConfigFile();
|
|
70
|
+
if (config.minimumPackageAgeHours) {
|
|
71
|
+
const validated = validateMinimumPackageAgeHours(
|
|
72
|
+
config.minimumPackageAgeHours
|
|
73
|
+
);
|
|
74
|
+
if (validated !== undefined) {
|
|
75
|
+
return validated;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {import("../api/aikido.js").MalwarePackage[]} data
|
|
83
|
+
* @param {string | number} version
|
|
84
|
+
*
|
|
85
|
+
* @returns {void}
|
|
86
|
+
*/
|
|
87
|
+
export function writeDatabaseToLocalCache(data, version) {
|
|
88
|
+
try {
|
|
89
|
+
const databasePath = getDatabasePath();
|
|
90
|
+
const versionPath = getDatabaseVersionPath();
|
|
91
|
+
|
|
92
|
+
fs.writeFileSync(databasePath, JSON.stringify(data));
|
|
93
|
+
fs.writeFileSync(versionPath, version.toString());
|
|
94
|
+
} catch {
|
|
95
|
+
ui.writeWarning(
|
|
96
|
+
"Failed to write malware database to local cache, next time the database will be fetched from the server again."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @returns {{malwareDatabase: import("../api/aikido.js").MalwarePackage[] | null, version: string | null}}
|
|
103
|
+
*/
|
|
104
|
+
export function readDatabaseFromLocalCache() {
|
|
105
|
+
try {
|
|
106
|
+
const databasePath = getDatabasePath();
|
|
107
|
+
if (!fs.existsSync(databasePath)) {
|
|
108
|
+
return {
|
|
109
|
+
malwareDatabase: null,
|
|
110
|
+
version: null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const data = fs.readFileSync(databasePath, "utf8");
|
|
114
|
+
const malwareDatabase = JSON.parse(data);
|
|
115
|
+
const versionPath = getDatabaseVersionPath();
|
|
116
|
+
let version = null;
|
|
117
|
+
if (fs.existsSync(versionPath)) {
|
|
118
|
+
version = fs.readFileSync(versionPath, "utf8").trim();
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
malwareDatabase: malwareDatabase,
|
|
122
|
+
version: version,
|
|
123
|
+
};
|
|
124
|
+
} catch {
|
|
125
|
+
ui.writeWarning(
|
|
126
|
+
"Failed to read malware database from local cache. Continuing without local cache."
|
|
127
|
+
);
|
|
128
|
+
return {
|
|
129
|
+
malwareDatabase: null,
|
|
130
|
+
version: null,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @returns {SafeChainConfig}
|
|
137
|
+
*/
|
|
138
|
+
function readConfigFile() {
|
|
139
|
+
const configFilePath = getConfigFilePath();
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(configFilePath)) {
|
|
142
|
+
return {
|
|
143
|
+
scanTimeout: undefined,
|
|
144
|
+
minimumPackageAgeHours: undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const data = fs.readFileSync(configFilePath, "utf8");
|
|
150
|
+
return JSON.parse(data);
|
|
151
|
+
} catch {
|
|
152
|
+
return {
|
|
153
|
+
scanTimeout: undefined,
|
|
154
|
+
minimumPackageAgeHours: undefined,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @returns {string}
|
|
161
|
+
*/
|
|
162
|
+
function getDatabasePath() {
|
|
163
|
+
const aikidoDir = getAikidoDirectory();
|
|
164
|
+
const ecosystem = getEcoSystem();
|
|
165
|
+
return path.join(aikidoDir, `malwareDatabase_${ecosystem}.json`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getDatabaseVersionPath() {
|
|
169
|
+
const aikidoDir = getAikidoDirectory();
|
|
170
|
+
const ecosystem = getEcoSystem();
|
|
171
|
+
return path.join(aikidoDir, `version_${ecosystem}.txt`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @returns {string}
|
|
176
|
+
*/
|
|
177
|
+
function getConfigFilePath() {
|
|
178
|
+
return path.join(getAikidoDirectory(), "config.json");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @returns {string}
|
|
183
|
+
*/
|
|
184
|
+
function getAikidoDirectory() {
|
|
185
|
+
const homeDir = os.homedir();
|
|
186
|
+
const aikidoDir = path.join(homeDir, ".aikido");
|
|
187
|
+
|
|
188
|
+
if (!fs.existsSync(aikidoDir)) {
|
|
189
|
+
fs.mkdirSync(aikidoDir, { recursive: true });
|
|
190
|
+
}
|
|
191
|
+
return aikidoDir;
|
|
192
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as cliArguments from "./cliArguments.js";
|
|
2
|
+
import * as configFile from "./configFile.js";
|
|
3
|
+
import * as environmentVariables from "./environmentVariables.js";
|
|
4
|
+
|
|
5
|
+
export const LOGGING_SILENT = "silent";
|
|
6
|
+
export const LOGGING_NORMAL = "normal";
|
|
7
|
+
export const LOGGING_VERBOSE = "verbose";
|
|
8
|
+
|
|
9
|
+
export function getLoggingLevel() {
|
|
10
|
+
const level = cliArguments.getLoggingLevel();
|
|
11
|
+
|
|
12
|
+
if (level === LOGGING_SILENT) {
|
|
13
|
+
return LOGGING_SILENT;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (level === LOGGING_VERBOSE) {
|
|
17
|
+
return LOGGING_VERBOSE;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return LOGGING_NORMAL;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ECOSYSTEM_JS = "js";
|
|
24
|
+
export const ECOSYSTEM_PY = "py";
|
|
25
|
+
|
|
26
|
+
// Default to JavaScript ecosystem
|
|
27
|
+
const ecosystemSettings = {
|
|
28
|
+
ecoSystem: ECOSYSTEM_JS,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** @returns {string} - The current ecosystem setting (ECOSYSTEM_JS or ECOSYSTEM_PY) */
|
|
32
|
+
export function getEcoSystem() {
|
|
33
|
+
return ecosystemSettings.ecoSystem;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} setting - The ecosystem to set (ECOSYSTEM_JS or ECOSYSTEM_PY)
|
|
37
|
+
*/
|
|
38
|
+
export function setEcoSystem(setting) {
|
|
39
|
+
ecosystemSettings.ecoSystem = setting;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const defaultMinimumPackageAge = 24;
|
|
43
|
+
/** @returns {number} */
|
|
44
|
+
export function getMinimumPackageAgeHours() {
|
|
45
|
+
// Priority 1: CLI argument
|
|
46
|
+
const cliValue = validateMinimumPackageAgeHours(
|
|
47
|
+
cliArguments.getMinimumPackageAgeHours()
|
|
48
|
+
);
|
|
49
|
+
if (cliValue !== undefined) {
|
|
50
|
+
return cliValue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Priority 2: Environment variable
|
|
54
|
+
const envValue = validateMinimumPackageAgeHours(
|
|
55
|
+
environmentVariables.getMinimumPackageAgeHours()
|
|
56
|
+
);
|
|
57
|
+
if (envValue !== undefined) {
|
|
58
|
+
return envValue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Priority 3: Config file
|
|
62
|
+
const configValue = configFile.getMinimumPackageAgeHours();
|
|
63
|
+
if (configValue !== undefined) {
|
|
64
|
+
return configValue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return defaultMinimumPackageAge;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {string | undefined} value
|
|
72
|
+
* @returns {number | undefined}
|
|
73
|
+
*/
|
|
74
|
+
function validateMinimumPackageAgeHours(value) {
|
|
75
|
+
if (!value) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const numericValue = Number(value);
|
|
80
|
+
if (Number.isNaN(numericValue)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (numericValue > 0) {
|
|
85
|
+
return numericValue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const defaultSkipMinimumPackageAge = false;
|
|
92
|
+
export function skipMinimumPackageAge() {
|
|
93
|
+
const cliValue = cliArguments.getSkipMinimumPackageAge();
|
|
94
|
+
|
|
95
|
+
if (cliValue === true) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return defaultSkipMinimumPackageAge;
|
|
100
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// oxlint-disable no-console
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { isCi } from "./environment.js";
|
|
4
|
+
import {
|
|
5
|
+
getLoggingLevel,
|
|
6
|
+
LOGGING_SILENT,
|
|
7
|
+
LOGGING_VERBOSE,
|
|
8
|
+
} from "../config/settings.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @type {{ bufferOutput: boolean, bufferedMessages:(() => void)[]}}
|
|
12
|
+
*/
|
|
13
|
+
const state = {
|
|
14
|
+
bufferOutput: false,
|
|
15
|
+
bufferedMessages: [],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function isSilentMode() {
|
|
19
|
+
return getLoggingLevel() === LOGGING_SILENT;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isVerboseMode() {
|
|
23
|
+
return getLoggingLevel() === LOGGING_VERBOSE;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function emptyLine() {
|
|
27
|
+
if (isSilentMode()) return;
|
|
28
|
+
|
|
29
|
+
writeInformation("");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} message
|
|
34
|
+
* @param {...any} optionalParams
|
|
35
|
+
* @returns {void}
|
|
36
|
+
*/
|
|
37
|
+
function writeInformation(message, ...optionalParams) {
|
|
38
|
+
if (isSilentMode()) return;
|
|
39
|
+
|
|
40
|
+
writeOrBuffer(() => console.log(message, ...optionalParams));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} message
|
|
45
|
+
* @param {...any} optionalParams
|
|
46
|
+
* @returns {void}
|
|
47
|
+
*/
|
|
48
|
+
function writeWarning(message, ...optionalParams) {
|
|
49
|
+
if (isSilentMode()) return;
|
|
50
|
+
|
|
51
|
+
if (!isCi()) {
|
|
52
|
+
message = chalk.yellow(message);
|
|
53
|
+
}
|
|
54
|
+
writeOrBuffer(() => console.warn(message, ...optionalParams));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} message
|
|
59
|
+
* @param {...any} optionalParams
|
|
60
|
+
* @returns {void}
|
|
61
|
+
*/
|
|
62
|
+
function writeError(message, ...optionalParams) {
|
|
63
|
+
if (!isCi()) {
|
|
64
|
+
message = chalk.red(message);
|
|
65
|
+
}
|
|
66
|
+
writeOrBuffer(() => console.error(message, ...optionalParams));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeExitWithoutInstallingMaliciousPackages() {
|
|
70
|
+
let message = "Safe-chain: Exiting without installing malicious packages.";
|
|
71
|
+
if (!isCi()) {
|
|
72
|
+
message = chalk.red(message);
|
|
73
|
+
}
|
|
74
|
+
writeOrBuffer(() => console.error(message));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} message
|
|
79
|
+
* @param {...any} optionalParams
|
|
80
|
+
* @returns {void}
|
|
81
|
+
*/
|
|
82
|
+
function writeVerbose(message, ...optionalParams) {
|
|
83
|
+
if (!isVerboseMode()) return;
|
|
84
|
+
|
|
85
|
+
writeOrBuffer(() => console.log(message, ...optionalParams));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
*
|
|
90
|
+
* @param {() => void} messageFunction
|
|
91
|
+
*/
|
|
92
|
+
function writeOrBuffer(messageFunction) {
|
|
93
|
+
if (state.bufferOutput) {
|
|
94
|
+
state.bufferedMessages.push(messageFunction);
|
|
95
|
+
} else {
|
|
96
|
+
messageFunction();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function startBufferingLogs() {
|
|
101
|
+
state.bufferOutput = true;
|
|
102
|
+
state.bufferedMessages = [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function writeBufferedLogsAndStopBuffering() {
|
|
106
|
+
state.bufferOutput = false;
|
|
107
|
+
for (const log of state.bufferedMessages) {
|
|
108
|
+
log();
|
|
109
|
+
}
|
|
110
|
+
state.bufferedMessages = [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const ui = {
|
|
114
|
+
writeVerbose,
|
|
115
|
+
writeInformation,
|
|
116
|
+
writeWarning,
|
|
117
|
+
writeError,
|
|
118
|
+
writeExitWithoutInstallingMaliciousPackages,
|
|
119
|
+
emptyLine,
|
|
120
|
+
startBufferingLogs,
|
|
121
|
+
writeBufferedLogsAndStopBuffering,
|
|
122
|
+
};
|