mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge branch 'main' into feature/minimum-package-age-from-list
This commit is contained in:
commit
e9db22eb50
13 changed files with 145 additions and 31 deletions
5
.github/workflows/build-and-release.yml
vendored
5
.github/workflows/build-and-release.yml
vendored
|
|
@ -12,7 +12,7 @@ permissions:
|
||||||
jobs:
|
jobs:
|
||||||
set-version:
|
set-version:
|
||||||
name: Set version number
|
name: Set version number
|
||||||
runs-on: ubuntu-latest
|
runs-on: open-source-releaser
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.get_version.outputs.tag }}
|
version: ${{ steps.get_version.outputs.tag }}
|
||||||
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
|
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
|
||||||
|
|
@ -44,8 +44,7 @@ jobs:
|
||||||
publish-binaries:
|
publish-binaries:
|
||||||
name: Publish to GitHub release
|
name: Publish to GitHub release
|
||||||
needs: [set-version, create-binaries]
|
needs: [set-version, create-binaries]
|
||||||
runs-on: ubuntu-latest
|
runs-on: open-source-releaser
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
|
||||||
4
.github/workflows/create-artifact.yml
vendored
4
.github/workflows/create-artifact.yml
vendored
|
|
@ -78,7 +78,9 @@ jobs:
|
||||||
|
|
||||||
- name: Set the version in safe-chain package
|
- name: Set the version in safe-chain package
|
||||||
if: inputs.version != ''
|
if: inputs.version != ''
|
||||||
run: npm --no-git-tag-version version ${{ inputs.version }} --workspace=packages/safe-chain --ignore-scripts
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
run: npm --no-git-tag-version version $VERSION --workspace=packages/safe-chain --ignore-scripts
|
||||||
|
|
||||||
- name: Create binary
|
- name: Create binary
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
- ✅ **Block malware on developer laptops and CI/CD**
|
- ✅ **Block malware on developer laptops and CI/CD**
|
||||||
- ✅ **Supports npm and PyPI** more package managers coming
|
- ✅ **Supports npm and PyPI** more package managers coming
|
||||||
- ✅ **Blocks packages newer than 24 hours** without breaking your build
|
- ✅ **Blocks packages newer than 48 hours** without breaking your build
|
||||||
- ✅ **Tokenless, free, no build data shared**
|
- ✅ **Tokenless, free, no build data shared**
|
||||||
|
|
||||||
Aikido Safe Chain supports the following package managers:
|
Aikido Safe Chain supports the following package managers:
|
||||||
|
|
@ -118,7 +118,7 @@ For npm packages, Safe Chain applies minimum package age checks in two ways:
|
||||||
- During normal package resolution, Safe Chain suppresses versions that are newer than the configured minimum age from the package metadata returned by the registry.
|
- During normal package resolution, Safe Chain suppresses versions that are newer than the configured minimum age from the package metadata returned by the registry.
|
||||||
- For direct package download requests that bypass that metadata flow, Safe Chain can block the request itself using a cached list of newly released packages.
|
- For direct package download requests that bypass that metadata flow, Safe Chain can block the request itself using a cached list of newly released packages.
|
||||||
|
|
||||||
By default, the minimum package age is 24 hours. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can configure this threshold or bypass this protection entirely - see the [Minimum Package Age Configuration](#minimum-package-age) section below.
|
By default, the minimum package age is 48 hours. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can configure this threshold or bypass this protection entirely - see the [Minimum Package Age Configuration](#minimum-package-age) section below.
|
||||||
|
|
||||||
⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to Python package managers (uv, pip, pip3, poetry, pipx).
|
⚠️ This feature **only applies to npm-based package managers** (npm, npx, yarn, pnpm, pnpx, bun, bunx) and does not apply to Python package managers (uv, pip, pip3, poetry, pipx).
|
||||||
|
|
||||||
|
|
@ -188,7 +188,7 @@ You can set the logging level through multiple sources (in order of priority):
|
||||||
|
|
||||||
## Minimum Package Age
|
## Minimum Package Age
|
||||||
|
|
||||||
You can configure how long packages must exist before Safe Chain allows their installation. By default, packages must be at least 24 hours old before they can be installed through npm-based package managers.
|
You can configure how long packages must exist before Safe Chain allows their installation. By default, packages must be at least 48 hours old before they can be installed through npm-based package managers.
|
||||||
|
|
||||||
For npm-based package managers, this check currently has two enforcement modes:
|
For npm-based package managers, this check currently has two enforcement modes:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export function setEcoSystem(setting) {
|
||||||
ecosystemSettings.ecoSystem = setting;
|
ecosystemSettings.ecoSystem = setting;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultMinimumPackageAge = 24;
|
const defaultMinimumPackageAge = 48;
|
||||||
/** @returns {number} */
|
/** @returns {number} */
|
||||||
export function getMinimumPackageAgeHours() {
|
export function getMinimumPackageAgeHours() {
|
||||||
// Priority 1: CLI argument
|
// Priority 1: CLI argument
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,18 @@ import { describe, it, after } from "node:test";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { unlinkSync } from "node:fs";
|
import { unlinkSync, writeFileSync } from "node:fs";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
import {
|
import {
|
||||||
DOWNLOAD_URLS,
|
DOWNLOAD_URLS,
|
||||||
downloadFile,
|
|
||||||
verifyChecksum,
|
verifyChecksum,
|
||||||
} from "./downloadAgent.js";
|
} from "./downloadAgent.js";
|
||||||
|
|
||||||
describe("downloadAgent checksums", { timeout: 120_000 }, () => {
|
describe("downloadAgent", () => {
|
||||||
const downloadedFiles = [];
|
const tempFiles = [];
|
||||||
|
|
||||||
after(() => {
|
after(() => {
|
||||||
for (const file of downloadedFiles) {
|
for (const file of tempFiles) {
|
||||||
try {
|
try {
|
||||||
unlinkSync(file);
|
unlinkSync(file);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -24,22 +24,33 @@ describe("downloadAgent checksums", { timeout: 120_000 }, () => {
|
||||||
|
|
||||||
for (const [platform, architectures] of Object.entries(DOWNLOAD_URLS)) {
|
for (const [platform, architectures] of Object.entries(DOWNLOAD_URLS)) {
|
||||||
for (const [arch, { url, checksum }] of Object.entries(architectures)) {
|
for (const [arch, { url, checksum }] of Object.entries(architectures)) {
|
||||||
it(`${platform}/${arch} checksum matches`, async () => {
|
it(`${platform}/${arch} has a valid download definition`, () => {
|
||||||
const destPath = join(
|
assert.match(
|
||||||
tmpdir(),
|
url,
|
||||||
`safe-chain-test-${platform}-${arch}-${Date.now()}`
|
/^https:\/\/github\.com\/AikidoSec\/safechain-internals\/releases\/download\/v\d+\.\d+\.\d+\/.+/,
|
||||||
);
|
|
||||||
downloadedFiles.push(destPath);
|
|
||||||
|
|
||||||
await downloadFile(url, destPath);
|
|
||||||
|
|
||||||
const isValid = await verifyChecksum(destPath, checksum);
|
|
||||||
assert.strictEqual(
|
|
||||||
isValid,
|
|
||||||
true,
|
|
||||||
`Checksum mismatch for ${platform}/${arch} (${url})`
|
|
||||||
);
|
);
|
||||||
|
assert.match(checksum, /^sha256:[a-f0-9]{64}$/);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it("verifies checksum for a local file", async () => {
|
||||||
|
const destPath = join(tmpdir(), `safe-chain-test-${Date.now()}`);
|
||||||
|
tempFiles.push(destPath);
|
||||||
|
|
||||||
|
writeFileSync(destPath, "safe-chain-test");
|
||||||
|
|
||||||
|
const expectedHash = createHash("sha256")
|
||||||
|
.update("safe-chain-test")
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await verifyChecksum(destPath, `sha256:${expectedHash}`),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
await verifyChecksum(destPath, `sha256:${"0".repeat(64)}`),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -91,9 +91,7 @@ async function setupShell(shell) {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ui.writeError(
|
ui.writeError(
|
||||||
`${chalk.bold("- " + shell.name + ":")} ${chalk.red(
|
`${chalk.bold("- " + shell.name + ":")} ${chalk.red("Setup failed")}`,
|
||||||
"Setup failed",
|
|
||||||
)}. Please check your ${shell.name} configuration.`,
|
|
||||||
);
|
);
|
||||||
if (error) {
|
if (error) {
|
||||||
let message = ` Error: ${error.message}`;
|
let message = ` Error: ${error.message}`;
|
||||||
|
|
@ -102,6 +100,12 @@ async function setupShell(shell) {
|
||||||
}
|
}
|
||||||
ui.writeError(message);
|
ui.writeError(message);
|
||||||
}
|
}
|
||||||
|
ui.emptyLine();
|
||||||
|
ui.writeInformation(` ${chalk.bold("To set up manually:")}`);
|
||||||
|
for (const instruction of shell.getManualSetupInstructions()) {
|
||||||
|
ui.writeInformation(` ${instruction}`);
|
||||||
|
}
|
||||||
|
ui.emptyLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import { ui } from "../environment/userInteraction.js";
|
||||||
* @property {() => boolean} isInstalled
|
* @property {() => boolean} isInstalled
|
||||||
* @property {(tools: import("./helpers.js").AikidoTool[]) => boolean|Promise<boolean>} setup
|
* @property {(tools: import("./helpers.js").AikidoTool[]) => boolean|Promise<boolean>} setup
|
||||||
* @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} teardown
|
* @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} teardown
|
||||||
|
* @property {() => string[]} getManualSetupInstructions
|
||||||
|
* @property {() => string[]} getManualTeardownInstructions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,22 @@ function cygpathw(path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getManualTeardownInstructions() {
|
||||||
|
return [
|
||||||
|
`Remove the following line from your ~/.bashrc file:`,
|
||||||
|
` source ~/.safe-chain/scripts/init-posix.sh`,
|
||||||
|
`Then restart your terminal or run: source ~/.bashrc`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManualSetupInstructions() {
|
||||||
|
return [
|
||||||
|
`Add the following line to your ~/.bashrc file:`,
|
||||||
|
` source ~/.safe-chain/scripts/init-posix.sh`,
|
||||||
|
`Then restart your terminal or run: source ~/.bashrc`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("../shellDetection.js").Shell}
|
* @type {import("../shellDetection.js").Shell}
|
||||||
*/
|
*/
|
||||||
|
|
@ -131,4 +147,6 @@ export default {
|
||||||
isInstalled,
|
isInstalled,
|
||||||
setup,
|
setup,
|
||||||
teardown,
|
teardown,
|
||||||
|
getManualSetupInstructions,
|
||||||
|
getManualTeardownInstructions,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,22 @@ function getStartupFile() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getManualTeardownInstructions() {
|
||||||
|
return [
|
||||||
|
`Remove the following line from your ~/.config/fish/config.fish file:`,
|
||||||
|
` source ~/.safe-chain/scripts/init-fish.fish`,
|
||||||
|
`Then restart your terminal or run: source ~/.config/fish/config.fish`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManualSetupInstructions() {
|
||||||
|
return [
|
||||||
|
`Add the following line to your ~/.config/fish/config.fish file:`,
|
||||||
|
` source ~/.safe-chain/scripts/init-fish.fish`,
|
||||||
|
`Then restart your terminal or run: source ~/.config/fish/config.fish`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("../shellDetection.js").Shell}
|
* @type {import("../shellDetection.js").Shell}
|
||||||
*/
|
*/
|
||||||
|
|
@ -74,4 +90,6 @@ export default {
|
||||||
isInstalled,
|
isInstalled,
|
||||||
setup,
|
setup,
|
||||||
teardown,
|
teardown,
|
||||||
|
getManualSetupInstructions,
|
||||||
|
getManualTeardownInstructions,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,22 @@ function getStartupFile() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getManualTeardownInstructions() {
|
||||||
|
return [
|
||||||
|
`Remove the following line from your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
||||||
|
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
||||||
|
`Then restart your terminal or run: . $PROFILE`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManualSetupInstructions() {
|
||||||
|
return [
|
||||||
|
`Add the following line to your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
||||||
|
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
||||||
|
`Then restart your terminal or run: . $PROFILE`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("../shellDetection.js").Shell}
|
* @type {import("../shellDetection.js").Shell}
|
||||||
*/
|
*/
|
||||||
|
|
@ -79,4 +95,6 @@ export default {
|
||||||
isInstalled,
|
isInstalled,
|
||||||
setup,
|
setup,
|
||||||
teardown,
|
teardown,
|
||||||
|
getManualSetupInstructions,
|
||||||
|
getManualTeardownInstructions,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,22 @@ function getStartupFile() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getManualTeardownInstructions() {
|
||||||
|
return [
|
||||||
|
`Remove the following line from your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
||||||
|
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
||||||
|
`Then restart your terminal or run: . $PROFILE`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManualSetupInstructions() {
|
||||||
|
return [
|
||||||
|
`Add the following line to your PowerShell profile (run "echo $PROFILE" to find its location):`,
|
||||||
|
` . "$HOME\\.safe-chain\\scripts\\init-pwsh.ps1"`,
|
||||||
|
`Then restart your terminal or run: . $PROFILE`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("../shellDetection.js").Shell}
|
* @type {import("../shellDetection.js").Shell}
|
||||||
*/
|
*/
|
||||||
|
|
@ -79,4 +95,6 @@ export default {
|
||||||
isInstalled,
|
isInstalled,
|
||||||
setup,
|
setup,
|
||||||
teardown,
|
teardown,
|
||||||
|
getManualSetupInstructions,
|
||||||
|
getManualTeardownInstructions,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,27 @@ function getStartupFile() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getManualTeardownInstructions() {
|
||||||
|
return [
|
||||||
|
`Remove the following line from your ~/.zshrc file:`,
|
||||||
|
` source ~/.safe-chain/scripts/init-posix.sh`,
|
||||||
|
`Then restart your terminal or run: source ~/.zshrc`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getManualSetupInstructions() {
|
||||||
|
return [
|
||||||
|
`Add the following line to your ~/.zshrc file:`,
|
||||||
|
` source ~/.safe-chain/scripts/init-posix.sh`,
|
||||||
|
`Then restart your terminal or run: source ~/.zshrc`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: shellName,
|
name: shellName,
|
||||||
isInstalled,
|
isInstalled,
|
||||||
setup,
|
setup,
|
||||||
teardown,
|
teardown,
|
||||||
|
getManualSetupInstructions,
|
||||||
|
getManualTeardownInstructions,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,14 @@ export async function teardown() {
|
||||||
ui.writeError(
|
ui.writeError(
|
||||||
`${chalk.bold("- " + shell.name + ":")} ${chalk.red(
|
`${chalk.bold("- " + shell.name + ":")} ${chalk.red(
|
||||||
"Teardown failed"
|
"Teardown failed"
|
||||||
)}. Please check your ${shell.name} configuration.`
|
)}`
|
||||||
);
|
);
|
||||||
|
ui.emptyLine();
|
||||||
|
ui.writeInformation(` ${chalk.bold("To tear down manually:")}`);
|
||||||
|
for (const instruction of shell.getManualTeardownInstructions()) {
|
||||||
|
ui.writeInformation(` ${instruction}`);
|
||||||
|
}
|
||||||
|
ui.emptyLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue