mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Merge remote-tracking branch 'origin/main' into bug/win32-command-parsing
This commit is contained in:
commit
a43c28fdec
18 changed files with 1295 additions and 424 deletions
115
.github/workflows/build-and-release.yml
vendored
115
.github/workflows/build-and-release.yml
vendored
|
|
@ -11,26 +11,97 @@ permissions:
|
|||
|
||||
jobs:
|
||||
set-version:
|
||||
name: Set version number
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.tag }}
|
||||
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set version number
|
||||
id: get_version
|
||||
run: |
|
||||
version="${{ github.ref_name }}"
|
||||
echo "tag=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check if pre-release
|
||||
id: check_prerelease
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
IS_PRERELEASE=$(gh release view ${{ steps.get_version.outputs.tag }} --json isPrerelease --jq '.isPrerelease')
|
||||
echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT
|
||||
echo "Release ${{ steps.get_version.outputs.tag }} is pre-release: $IS_PRERELEASE"
|
||||
|
||||
create-binaries:
|
||||
needs: set-version
|
||||
uses: ./.github/workflows/create-artifact.yml
|
||||
with:
|
||||
version: ${{ needs.set-version.outputs.version }}
|
||||
|
||||
build:
|
||||
publish-binaries:
|
||||
name: Publish to GitHub release
|
||||
needs: [set-version, create-binaries]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download all binary artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: binaries/
|
||||
pattern: safe-chain-*
|
||||
merge-multiple: false
|
||||
|
||||
- name: Rename binaries to include platform and architecture
|
||||
run: |
|
||||
mkdir release-artifacts
|
||||
mv binaries/safe-chain-macos-x64/safe-chain release-artifacts/safe-chain-macos-x64
|
||||
mv binaries/safe-chain-macos-arm64/safe-chain release-artifacts/safe-chain-macos-arm64
|
||||
mv binaries/safe-chain-linux-x64/safe-chain release-artifacts/safe-chain-linux-x64
|
||||
mv binaries/safe-chain-linux-arm64/safe-chain release-artifacts/safe-chain-linux-arm64
|
||||
mv binaries/safe-chain-linuxstatic-x64/safe-chain release-artifacts/safe-chain-linuxstatic-x64
|
||||
mv binaries/safe-chain-linuxstatic-arm64/safe-chain release-artifacts/safe-chain-linuxstatic-arm64
|
||||
mv binaries/safe-chain-win-x64/safe-chain.exe release-artifacts/safe-chain-win-x64.exe
|
||||
mv binaries/safe-chain-win-arm64/safe-chain.exe release-artifacts/safe-chain-win-arm64.exe
|
||||
|
||||
- name: Move install scripts and hard-code version
|
||||
env:
|
||||
VERSION: ${{ needs.set-version.outputs.version }}
|
||||
run: |
|
||||
sed "s/\$(fetch_latest_version)/${VERSION}/" install-scripts/install-safe-chain.sh > release-artifacts/install-safe-chain.sh
|
||||
sed "s/\$Version = Get-LatestVersion/\$Version = \"${VERSION}\"/" install-scripts/install-safe-chain.ps1 > release-artifacts/install-safe-chain.ps1
|
||||
cp install-scripts/uninstall-safe-chain.sh release-artifacts/uninstall-safe-chain.sh
|
||||
cp install-scripts/uninstall-safe-chain.ps1 release-artifacts/uninstall-safe-chain.ps1
|
||||
|
||||
- name: Upload binaries to existing GitHub Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload ${{ needs.set-version.outputs.version }} \
|
||||
release-artifacts/safe-chain-macos-x64 \
|
||||
release-artifacts/safe-chain-macos-arm64 \
|
||||
release-artifacts/safe-chain-linux-x64 \
|
||||
release-artifacts/safe-chain-linux-arm64 \
|
||||
release-artifacts/safe-chain-linuxstatic-x64 \
|
||||
release-artifacts/safe-chain-linuxstatic-arm64 \
|
||||
release-artifacts/safe-chain-win-x64.exe \
|
||||
release-artifacts/safe-chain-win-arm64.exe \
|
||||
release-artifacts/install-safe-chain.sh \
|
||||
release-artifacts/install-safe-chain.ps1 \
|
||||
release-artifacts/uninstall-safe-chain.sh \
|
||||
release-artifacts/uninstall-safe-chain.ps1
|
||||
|
||||
publish-npm:
|
||||
name: Publish to npm
|
||||
needs: [set-version, create-binaries]
|
||||
if: needs.set-version.outputs.is_prerelease != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
|
@ -65,45 +136,3 @@ jobs:
|
|||
run: |
|
||||
echo "Publishing version ${{ needs.set-version.outputs.version }} to NPM"
|
||||
npm publish --workspace=packages/safe-chain --access public --provenance
|
||||
|
||||
- name: Download all binary artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: binaries/
|
||||
pattern: safe-chain-*
|
||||
merge-multiple: false
|
||||
|
||||
- name: Rename binaries to include platform and architecture
|
||||
run: |
|
||||
mkdir release-artifacts
|
||||
mv binaries/safe-chain-macos-x64/safe-chain release-artifacts/safe-chain-macos-x64
|
||||
mv binaries/safe-chain-macos-arm64/safe-chain release-artifacts/safe-chain-macos-arm64
|
||||
mv binaries/safe-chain-linux-x64/safe-chain release-artifacts/safe-chain-linux-x64
|
||||
mv binaries/safe-chain-linux-arm64/safe-chain release-artifacts/safe-chain-linux-arm64
|
||||
mv binaries/safe-chain-win-x64/safe-chain.exe release-artifacts/safe-chain-win-x64.exe
|
||||
mv binaries/safe-chain-win-arm64/safe-chain.exe release-artifacts/safe-chain-win-arm64.exe
|
||||
|
||||
- name: Move install scripts and hard-code version
|
||||
env:
|
||||
VERSION: ${{ needs.set-version.outputs.version }}
|
||||
run: |
|
||||
sed "s/\$(fetch_latest_version)/${VERSION}/" install-scripts/install-safe-chain.sh > release-artifacts/install-safe-chain.sh
|
||||
sed "s/\$Version = Get-LatestVersion/\$Version = \"${VERSION}\"/" install-scripts/install-safe-chain.ps1 > release-artifacts/install-safe-chain.ps1
|
||||
cp install-scripts/uninstall-safe-chain.sh release-artifacts/uninstall-safe-chain.sh
|
||||
cp install-scripts/uninstall-safe-chain.ps1 release-artifacts/uninstall-safe-chain.ps1
|
||||
|
||||
- name: Upload binaries to existing GitHub Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload ${{ needs.set-version.outputs.version }} \
|
||||
release-artifacts/safe-chain-macos-x64 \
|
||||
release-artifacts/safe-chain-macos-arm64 \
|
||||
release-artifacts/safe-chain-linux-x64 \
|
||||
release-artifacts/safe-chain-linux-arm64 \
|
||||
release-artifacts/safe-chain-win-x64.exe \
|
||||
release-artifacts/safe-chain-win-arm64.exe \
|
||||
release-artifacts/install-safe-chain.sh \
|
||||
release-artifacts/install-safe-chain.ps1 \
|
||||
release-artifacts/uninstall-safe-chain.sh \
|
||||
release-artifacts/uninstall-safe-chain.ps1
|
||||
|
|
|
|||
10
.github/workflows/create-artifact.yml
vendored
10
.github/workflows/create-artifact.yml
vendored
|
|
@ -39,6 +39,16 @@ jobs:
|
|||
runner: ubuntu-24.04-arm
|
||||
target: node20-linux-arm64
|
||||
extension: ""
|
||||
- os: linuxstatic
|
||||
arch: x64
|
||||
runner: ubuntu-latest
|
||||
target: node20-linuxstatic-x64
|
||||
extension: ""
|
||||
- os: linuxstatic
|
||||
arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
target: node20-linuxstatic-arm64
|
||||
extension: ""
|
||||
- os: win
|
||||
arch: x64
|
||||
runner: windows-latest
|
||||
|
|
|
|||
60
README.md
60
README.md
|
|
@ -33,8 +33,6 @@ Aikido Safe Chain supports the following package managers:
|
|||
|
||||
Installing the Aikido Safe Chain is easy with our one-line installer.
|
||||
|
||||
> ⚠️ **Already installed via npm?** See the [migration guide](https://github.com/AikidoSec/safe-chain/blob/main/docs/npm-to-binary-migration.md) to switch to the binary version.
|
||||
|
||||
### Unix/Linux/macOS
|
||||
|
||||
```shell
|
||||
|
|
@ -71,7 +69,20 @@ You can find all available versions on the [releases page](https://github.com/Ai
|
|||
|
||||
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, bun, bunx, pip, pip3, poetry, uv and pipx are loaded correctly. If you do not restart your terminal, the aliases will not be available.
|
||||
|
||||
2. **Verify the installation** by running one of the following commands:
|
||||
2. **Verify the installation** by running the verification command:
|
||||
|
||||
```shell
|
||||
npm safe-chain-verify
|
||||
pnpm safe-chain-verify
|
||||
pip safe-chain-verify
|
||||
uv safe-chain-verify
|
||||
|
||||
# Any other supported package manager: {packagemanager} safe-chain-verify
|
||||
```
|
||||
|
||||
- The output should display "OK: Safe-chain works!" confirming that Aikido Safe Chain is properly installed and running.
|
||||
|
||||
3. **(Optional) Test malware blocking** by attempting to install a test package:
|
||||
|
||||
For JavaScript/Node.js:
|
||||
|
||||
|
|
@ -141,24 +152,37 @@ iex (iwr "https://github.com/AikidoSec/safe-chain/releases/latest/download/unins
|
|||
|
||||
## Logging
|
||||
|
||||
You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag:
|
||||
You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag or the `SAFE_CHAIN_LOGGING` environment variable.
|
||||
|
||||
- `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit.
|
||||
### Configuration Options
|
||||
|
||||
Example usage:
|
||||
You can set the logging level through multiple sources (in order of priority):
|
||||
|
||||
1. **CLI Argument** (highest priority):
|
||||
|
||||
- `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit.
|
||||
|
||||
```shell
|
||||
npm install express --safe-chain-logging=silent
|
||||
```
|
||||
|
||||
- `--safe-chain-logging=verbose` - Enables detailed diagnostic output from Aikido Safe Chain. Useful for troubleshooting issues or understanding what Safe Chain is doing behind the scenes.
|
||||
|
||||
Example usage:
|
||||
- `--safe-chain-logging=verbose` - Enables detailed diagnostic output from Aikido Safe Chain. Useful for troubleshooting issues or understanding what Safe Chain is doing behind the scenes.
|
||||
|
||||
```shell
|
||||
npm install express --safe-chain-logging=verbose
|
||||
```
|
||||
|
||||
2. **Environment Variable**:
|
||||
|
||||
```shell
|
||||
export SAFE_CHAIN_LOGGING=verbose
|
||||
npm install express
|
||||
```
|
||||
|
||||
Valid values: `silent`, `normal`, `verbose`
|
||||
|
||||
This is useful for setting a default logging level for all package manager commands in your terminal session or CI/CD environment.
|
||||
|
||||
## 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.
|
||||
|
|
@ -188,9 +212,14 @@ You can set the minimum package age through multiple sources (in order of priori
|
|||
}
|
||||
```
|
||||
|
||||
## Custom NPM Registries
|
||||
## Custom Registries
|
||||
|
||||
Configure Safe Chain to scan packages from custom or private npm registries.
|
||||
Configure Safe Chain to scan packages from custom or private registries.
|
||||
|
||||
Supported ecosystems:
|
||||
|
||||
- Node.js
|
||||
- Python
|
||||
|
||||
### Configuration Options
|
||||
|
||||
|
|
@ -200,6 +229,7 @@ You can set custom registries through environment variable or config file. Both
|
|||
|
||||
```shell
|
||||
export SAFE_CHAIN_NPM_CUSTOM_REGISTRIES="npm.company.com,registry.internal.net"
|
||||
export SAFE_CHAIN_PIP_CUSTOM_REGISTRIES="pip.company.com,registry.internal.net"
|
||||
```
|
||||
|
||||
2. **Config File** (`~/.aikido/config.json`):
|
||||
|
|
@ -208,6 +238,9 @@ You can set custom registries through environment variable or config file. Both
|
|||
{
|
||||
"npm": {
|
||||
"customRegistries": ["npm.company.com", "registry.internal.net"]
|
||||
},
|
||||
"pip": {
|
||||
"customRegistries": ["pip.company.com", "registry.internal.net"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -327,5 +360,8 @@ pipeline {
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
Having issues? See the [Troubleshooting Guide](https://github.com/AikidoSec/safe-chain/blob/main/docs/troubleshooting.md) for help with common problems.
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
# Migrating from npm global tool to binary installation
|
||||
|
||||
If you previously installed safe-chain as an npm global package, you need to migrate to the binary installation.
|
||||
|
||||
Depending on the version manager you're using, the uninstall process differs:
|
||||
|
||||
### Standard npm (no version manager)
|
||||
|
||||
1. **Clean up shell aliases:**
|
||||
|
||||
```bash
|
||||
safe-chain teardown
|
||||
```
|
||||
|
||||
2. **Restart your terminal**
|
||||
|
||||
3. **Uninstall the npm package:**
|
||||
|
||||
```bash
|
||||
npm uninstall -g @aikidosec/safe-chain
|
||||
```
|
||||
|
||||
4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation))
|
||||
|
||||
### nvm (Node Version Manager)
|
||||
|
||||
**Important:** nvm installs global packages separately for each Node version, so safe-chain must be uninstalled from each version where it was installed.
|
||||
|
||||
1. **Clean up shell aliases:**
|
||||
|
||||
```bash
|
||||
safe-chain teardown
|
||||
```
|
||||
|
||||
2. **Restart your terminal**
|
||||
|
||||
3. **Uninstall from all Node versions:**
|
||||
|
||||
**Option A** - Automated script (recommended):
|
||||
|
||||
```bash
|
||||
for version in $(nvm list | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+'); do nvm use $version && npm uninstall -g @aikidosec/safe-chain; done
|
||||
```
|
||||
|
||||
**Option B** - Manual per version:
|
||||
|
||||
```bash
|
||||
nvm use <version>
|
||||
npm uninstall -g @aikidosec/safe-chain
|
||||
```
|
||||
|
||||
Repeat for each Node version where safe-chain was installed.
|
||||
|
||||
4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation))
|
||||
|
||||
### Volta
|
||||
|
||||
1. **Clean up shell aliases:**
|
||||
|
||||
```bash
|
||||
safe-chain teardown
|
||||
```
|
||||
|
||||
2. **Restart your terminal**
|
||||
|
||||
3. **Uninstall the Volta package:**
|
||||
|
||||
```bash
|
||||
volta uninstall @aikidosec/safe-chain
|
||||
```
|
||||
|
||||
4. **Install the binary version** (see [Installation](https://github.com/AikidoSec/safe-chain/blob/main/README.md#installation))
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Shell aliases still present after migration
|
||||
|
||||
1. Run `safe-chain teardown` (if the binary is installed)
|
||||
2. Manually remove any safe-chain entries from your shell config files:
|
||||
- Bash: `~/.bashrc`
|
||||
- Zsh: `~/.zshrc`
|
||||
- Fish: `~/.config/fish/config.fish`
|
||||
- PowerShell: `$PROFILE`
|
||||
3. Restart your terminal
|
||||
4. Re-run the install script
|
||||
|
||||
### "command not found: safe-chain" after migration
|
||||
|
||||
The binary installation directory (`~/.safe-chain/bin`) may not be in your PATH. Restart your terminal. If the problem persists: re-run the installation of safe-chain.
|
||||
306
docs/troubleshooting.md
Normal file
306
docs/troubleshooting.md
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
# Troubleshooting
|
||||
|
||||
This guide helps you diagnose and resolve common issues with Aikido Safe Chain.
|
||||
|
||||
## Verification & Diagnostics
|
||||
|
||||
### Check Installation
|
||||
|
||||
```bash
|
||||
# Check version
|
||||
safe-chain --version
|
||||
```
|
||||
|
||||
### Verify Shell Integration
|
||||
|
||||
Run the verification command for your package manager:
|
||||
|
||||
```bash
|
||||
npm safe-chain-verify
|
||||
pnpm safe-chain-verify
|
||||
pip safe-chain-verify
|
||||
uv safe-chain-verify
|
||||
|
||||
# Any other supported package manager: {packagemanager} safe-chain-verify
|
||||
```
|
||||
|
||||
Expected output: `OK: Safe-chain works!`
|
||||
|
||||
### Test Malware Blocking
|
||||
|
||||
Verify that malware detection is working:
|
||||
|
||||
**For JavaScript/Node.js:**
|
||||
|
||||
```bash
|
||||
npm install safe-chain-test
|
||||
```
|
||||
|
||||
**For Python:**
|
||||
|
||||
```bash
|
||||
pip3 install safe-chain-pi-test
|
||||
```
|
||||
|
||||
These test packages are flagged as malware and should be blocked by Safe Chain.
|
||||
|
||||
**If the test package installs successfully instead of being blocked**, see [Malware Not Being Blocked](#malware-not-being-blocked) below.
|
||||
|
||||
### Logging Options
|
||||
|
||||
Use logging flags or environment variables to get more information:
|
||||
|
||||
```bash
|
||||
# Verbose mode - detailed diagnostic output for troubleshooting
|
||||
npm install express --safe-chain-logging=verbose
|
||||
|
||||
# Or set it globally for all commands in your session
|
||||
export SAFE_CHAIN_LOGGING=verbose
|
||||
npm install express
|
||||
|
||||
# Silent mode - suppress all output except malware blocking
|
||||
npm install express --safe-chain-logging=silent
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Malware Not Being Blocked
|
||||
|
||||
**Symptom:** Test malware packages (like `safe-chain-test`) install successfully when they should be blocked
|
||||
|
||||
**Most Common Cause:** The package is cached in your package manager's local store
|
||||
|
||||
Safe-chain blocks malicious packages by intercepting network requests to package registries using its proxy.
|
||||
|
||||
When a package is already cached locally, the package manager skips downloading it from the registry, which bypasses the proxy.
|
||||
|
||||
**Resolution Steps:**
|
||||
|
||||
1. **Clear your package manager's cache:**
|
||||
|
||||
```bash
|
||||
# For npm
|
||||
npm cache clean --force
|
||||
|
||||
# For pnpm
|
||||
pnpm store prune
|
||||
|
||||
# For yarn (classic)
|
||||
yarn cache clean
|
||||
|
||||
# For yarn (berry/v2+)
|
||||
yarn cache clean --all
|
||||
|
||||
# For bun
|
||||
bun pm cache rm
|
||||
```
|
||||
|
||||
> **⚠️ Warning:** Cache clearing is safe but will remove all cached packages. Subsequent installations will need to re-download packages. In CI/CD environments or monorepos, this may affect build times.
|
||||
|
||||
2. **Clean local installation artifacts:**
|
||||
|
||||
```bash
|
||||
# Remove node_modules if you want a completely fresh install
|
||||
rm -rf node_modules
|
||||
```
|
||||
|
||||
3. **Re-test malware blocking:**
|
||||
|
||||
```bash
|
||||
npm install safe-chain-test # Should be blocked
|
||||
```
|
||||
|
||||
### Shell Aliases Not Working After Installation
|
||||
|
||||
**Symptom:** Running `npm` shows regular npm instead of safe-chain wrapped version
|
||||
|
||||
**First step:** Restart your terminal (most common fix)
|
||||
|
||||
**Verify it's working:**
|
||||
|
||||
```bash
|
||||
type npm
|
||||
```
|
||||
|
||||
Should show: `npm is a function`
|
||||
|
||||
**If still not working:**
|
||||
|
||||
Check that your startup file sources safe-chain scripts from `~/.safe-chain/scripts/`:
|
||||
|
||||
- Bash: `~/.bashrc`
|
||||
- Zsh: `~/.zshrc`
|
||||
- Fish: `~/.config/fish/config.fish`
|
||||
- PowerShell: `$PROFILE`
|
||||
|
||||
### "Command Not Found: safe-chain"
|
||||
|
||||
**Symptom:** Binary not found in PATH
|
||||
|
||||
**First step:** Restart your terminal
|
||||
|
||||
**Check PATH:**
|
||||
|
||||
```bash
|
||||
echo $PATH
|
||||
```
|
||||
|
||||
Should include `~/.safe-chain/bin`
|
||||
|
||||
**If persists:** Re-run the installation script
|
||||
|
||||
### Shell Aliases Persist After Uninstallation
|
||||
|
||||
**Symptom:** safe-chain commands still active after running uninstall script
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Run `safe-chain teardown` (if binary still exists)
|
||||
2. Restart your terminal
|
||||
3. If still present, manually edit shell config files:
|
||||
- Bash: `~/.bashrc`
|
||||
- Zsh: `~/.zshrc`
|
||||
- Fish: `~/.config/fish/config.fish`
|
||||
- PowerShell: `$PROFILE`
|
||||
4. Remove lines that source scripts from `~/.safe-chain/scripts/`
|
||||
5. Restart terminal again
|
||||
|
||||
## Manual Verification Steps
|
||||
|
||||
### Check Installation Status
|
||||
|
||||
```bash
|
||||
# Check installation location (helps identify if installed via npm or as standalone binary)
|
||||
which safe-chain
|
||||
|
||||
# Verify binary exists
|
||||
ls ~/.safe-chain/bin/safe-chain
|
||||
|
||||
# Check version
|
||||
safe-chain --version
|
||||
|
||||
# Test shell integration
|
||||
type npm
|
||||
type pip
|
||||
```
|
||||
|
||||
**Expected `which` output:**
|
||||
|
||||
- Standalone binary (correct): `~/.safe-chain/bin/safe-chain` or `/Users/<username>/.safe-chain/bin/safe-chain`
|
||||
- npm global (outdated): path containing `node_modules` or nvm version paths
|
||||
|
||||
If `which` shows an npm installation, see [Check for Conflicting Installations](#check-for-conflicting-installations).
|
||||
|
||||
### Check Shell Integration
|
||||
|
||||
```bash
|
||||
# Which shell you're using
|
||||
echo $SHELL
|
||||
|
||||
# Check if startup file sources safe-chain
|
||||
# For Bash:
|
||||
grep safe-chain ~/.bashrc
|
||||
|
||||
# For Zsh:
|
||||
grep safe-chain ~/.zshrc
|
||||
|
||||
# For Fish:
|
||||
grep safe-chain ~/.config/fish/config.fish
|
||||
|
||||
# Verify scripts exist
|
||||
ls ~/.safe-chain/scripts/
|
||||
```
|
||||
|
||||
### Check for Conflicting Installations
|
||||
|
||||
> **Note:** The install/uninstall scripts automatically detect and remove conflicting installations, but you can manually check:
|
||||
|
||||
```bash
|
||||
# Check npm global
|
||||
npm list -g @aikidosec/safe-chain
|
||||
|
||||
# Check Volta
|
||||
volta list safe-chain
|
||||
|
||||
# Check nvm (all versions)
|
||||
for version in $(nvm list | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+'); do
|
||||
nvm exec "$version" npm list -g @aikidosec/safe-chain 2>/dev/null && echo "Found in $version"
|
||||
done
|
||||
```
|
||||
|
||||
## Manual Cleanup
|
||||
|
||||
> **Note:** The install and uninstall scripts automatically handle these cleanup steps. Use these manual commands only if automatic cleanup fails.
|
||||
|
||||
### Remove npm Global Installation
|
||||
|
||||
```bash
|
||||
npm uninstall -g @aikidosec/safe-chain
|
||||
```
|
||||
|
||||
### Remove Volta Installation
|
||||
|
||||
```bash
|
||||
volta uninstall @aikidosec/safe-chain
|
||||
```
|
||||
|
||||
### Remove nvm Installations (All Versions)
|
||||
|
||||
```bash
|
||||
# Automated approach
|
||||
for version in $(nvm list | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+'); do
|
||||
nvm exec "$version" npm uninstall -g @aikidosec/safe-chain
|
||||
done
|
||||
|
||||
# Or manual per version
|
||||
nvm use <version>
|
||||
npm uninstall -g @aikidosec/safe-chain
|
||||
```
|
||||
|
||||
### Clean Shell Configuration Files
|
||||
|
||||
Manually remove safe-chain entries from:
|
||||
|
||||
- Bash: `~/.bashrc`
|
||||
- Zsh: `~/.zshrc`
|
||||
- Fish: `~/.config/fish/config.fish`
|
||||
- PowerShell: `$PROFILE`
|
||||
|
||||
Look for and remove:
|
||||
|
||||
- Lines sourcing from `~/.safe-chain/scripts/`
|
||||
- Any safe-chain related function definitions
|
||||
|
||||
### Remove Installation Directory
|
||||
|
||||
```bash
|
||||
rm -rf ~/.safe-chain
|
||||
```
|
||||
|
||||
## Getting More Information
|
||||
|
||||
### Enable Verbose Logging
|
||||
|
||||
Get detailed diagnostic output using a CLI flag or environment variable:
|
||||
|
||||
```bash
|
||||
# Using CLI flag
|
||||
npm install express --safe-chain-logging=verbose
|
||||
pip install requests --safe-chain-logging=verbose
|
||||
|
||||
# Using environment variable (applies to all commands)
|
||||
export SAFE_CHAIN_LOGGING=verbose
|
||||
npm install express
|
||||
```
|
||||
|
||||
### Report Issues
|
||||
|
||||
If you encounter problems:
|
||||
|
||||
1. Visit [GitHub Issues](https://github.com/AikidoSec/safe-chain/issues)
|
||||
2. Include:
|
||||
- Operating system and version
|
||||
- Shell type and version
|
||||
- `safe-chain --version` output
|
||||
- Output from verification commands
|
||||
- Verbose logs of the failing command (add the `--safe-chain-logging=verbose` argument)
|
||||
|
|
@ -149,6 +149,20 @@ function Remove-VoltaInstallation {
|
|||
|
||||
# Main installation
|
||||
function Install-SafeChain {
|
||||
# Show deprecation warning if SAFE_CHAIN_VERSION is set
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:SAFE_CHAIN_VERSION)) {
|
||||
Write-Warn "SAFE_CHAIN_VERSION environment variable is deprecated."
|
||||
Write-Warn ""
|
||||
Write-Warn "Please use direct download URLs for version pinning instead:"
|
||||
Write-Warn ""
|
||||
if ($ci) {
|
||||
Write-Warn " iex `"& { `$(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/$env:SAFE_CHAIN_VERSION/install-safe-chain.ps1' -UseBasicParsing) } -ci`""
|
||||
} else {
|
||||
Write-Warn " iex (iwr `"https://github.com/AikidoSec/safe-chain/releases/download/$env:SAFE_CHAIN_VERSION/install-safe-chain.ps1`" -UseBasicParsing)"
|
||||
}
|
||||
Write-Warn ""
|
||||
}
|
||||
|
||||
# Fetch latest version if VERSION is not set
|
||||
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||
Write-Info "Fetching latest release version..."
|
||||
|
|
|
|||
|
|
@ -32,9 +32,16 @@ error() {
|
|||
}
|
||||
|
||||
# Detect OS
|
||||
# For legacy versions (when SAFE_CHAIN_VERSION is set), use 'linux' instead of 'linuxstatic'
|
||||
detect_os() {
|
||||
case "$(uname -s)" in
|
||||
Linux*) echo "linux" ;;
|
||||
Linux*)
|
||||
if [ -n "$SAFE_CHAIN_VERSION" ]; then
|
||||
echo "linux"
|
||||
else
|
||||
echo "linuxstatic"
|
||||
fi
|
||||
;;
|
||||
Darwin*) echo "macos" ;;
|
||||
*) error "Unsupported operating system: $(uname -s)" ;;
|
||||
esac
|
||||
|
|
@ -159,6 +166,66 @@ remove_volta_installation() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Check and uninstall nvm-managed package if present across all Node versions
|
||||
remove_nvm_installation() {
|
||||
# This script is run in sh shell for greatest compatibility.
|
||||
# Because nvm is usually setup in bash/zsh/fish startup scripts, we need to source it.
|
||||
# Otherwise it won't be available in sh.
|
||||
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||
# Source nvm to make it available in this script
|
||||
. "$HOME/.nvm/nvm.sh" >/dev/null 2>&1
|
||||
elif [ -s "$NVM_DIR/nvm.sh" ]; then
|
||||
. "$NVM_DIR/nvm.sh" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Check if nvm is now available
|
||||
if ! command_exists nvm; then
|
||||
return
|
||||
fi
|
||||
|
||||
nvm_versions=$(nvm list 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' || echo "")
|
||||
|
||||
if [ -z "$nvm_versions" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Track if we found any installations
|
||||
found_installation=false
|
||||
uninstall_failed=false
|
||||
current_version=$(nvm current 2>/dev/null || echo "")
|
||||
|
||||
# Check each version for safe-chain installation
|
||||
for version in $nvm_versions; do
|
||||
# Check if this version has safe-chain installed
|
||||
# Use nvm exec to run npm list in the context of that Node version
|
||||
if nvm exec "$version" npm list -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||
if [ "$found_installation" = false ]; then
|
||||
info "Detected nvm installation(s) of @aikidosec/safe-chain"
|
||||
info "Uninstalling from all Node versions..."
|
||||
found_installation=true
|
||||
fi
|
||||
|
||||
info " Removing from Node $version..."
|
||||
if nvm exec "$version" npm uninstall -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||
info " Successfully uninstalled from Node $version"
|
||||
else
|
||||
warn " Failed to uninstall from Node $version"
|
||||
uninstall_failed=true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Restore original Node version if it was set
|
||||
if [ -n "$current_version" ] && [ "$current_version" != "none" ] && [ "$current_version" != "system" ]; then
|
||||
nvm use "$current_version" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# If any uninstall failed, error out instead of continuing
|
||||
if [ "$uninstall_failed" = true ]; then
|
||||
error "Failed to uninstall @aikidosec/safe-chain from all nvm Node versions. Please uninstall manually and try again."
|
||||
fi
|
||||
}
|
||||
|
||||
# Parse command-line arguments
|
||||
parse_arguments() {
|
||||
for arg in "$@"; do
|
||||
|
|
@ -184,6 +251,20 @@ main() {
|
|||
# Parse command-line arguments
|
||||
parse_arguments "$@"
|
||||
|
||||
# Show deprecation warning if SAFE_CHAIN_VERSION is set
|
||||
if [ -n "$SAFE_CHAIN_VERSION" ]; then
|
||||
warn "SAFE_CHAIN_VERSION environment variable is deprecated."
|
||||
warn ""
|
||||
warn "Please use direct download URLs for version pinning instead:"
|
||||
warn ""
|
||||
if [ "$USE_CI_SETUP" = "true" ]; then
|
||||
warn " curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/${SAFE_CHAIN_VERSION}/install-safe-chain.sh | sh -s -- --ci"
|
||||
else
|
||||
warn " curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/${SAFE_CHAIN_VERSION}/install-safe-chain.sh | sh"
|
||||
fi
|
||||
warn ""
|
||||
fi
|
||||
|
||||
# Fetch latest version if VERSION is not set
|
||||
if [ -z "$VERSION" ]; then
|
||||
info "Fetching latest release version..."
|
||||
|
|
@ -204,9 +285,10 @@ main() {
|
|||
|
||||
info "$INSTALL_MSG"
|
||||
|
||||
# Check for existing safe-chain installation through npm or volta
|
||||
# Check for existing safe-chain installation through nvm, volta, or npm
|
||||
remove_npm_installation
|
||||
remove_volta_installation
|
||||
remove_nvm_installation
|
||||
|
||||
# Detect platform
|
||||
OS=$(detect_os)
|
||||
|
|
|
|||
|
|
@ -75,6 +75,68 @@ remove_volta_installation() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Check and uninstall nvm-managed package if present across all Node versions
|
||||
remove_nvm_installation() {
|
||||
# This script is run in sh shell for greatest compatibility.
|
||||
# Because nvm is usually setup in bash/zsh/fish startup scripts, we need to source it.
|
||||
# Otherwise it won't be available in sh.
|
||||
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||
# Source nvm to make it available in this script
|
||||
. "$HOME/.nvm/nvm.sh" >/dev/null 2>&1
|
||||
elif [ -s "$NVM_DIR/nvm.sh" ]; then
|
||||
. "$NVM_DIR/nvm.sh" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Check if nvm is now available
|
||||
if ! command_exists nvm; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Get list of installed Node versions
|
||||
nvm_versions=$(nvm list 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' || echo "")
|
||||
|
||||
if [ -z "$nvm_versions" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Track if we found any installations
|
||||
found_installation=false
|
||||
uninstall_failed=false
|
||||
current_version=$(nvm current 2>/dev/null || echo "")
|
||||
|
||||
# Check each version for safe-chain installation
|
||||
for version in $nvm_versions; do
|
||||
# Check if this version has safe-chain installed
|
||||
# Use nvm exec to run npm list in the context of that Node version
|
||||
if nvm exec "$version" npm list -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||
if [ "$found_installation" = false ]; then
|
||||
info "Detected nvm installation(s) of @aikidosec/safe-chain"
|
||||
info "Uninstalling from all Node versions..."
|
||||
found_installation=true
|
||||
fi
|
||||
|
||||
info " Removing from Node $version..."
|
||||
if nvm exec "$version" npm uninstall -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||
info " Successfully uninstalled from Node $version"
|
||||
else
|
||||
warn " Failed to uninstall from Node $version"
|
||||
uninstall_failed=true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Restore original Node version if it was set
|
||||
if [ -n "$current_version" ] && [ "$current_version" != "none" ] && [ "$current_version" != "system" ]; then
|
||||
nvm use "$current_version" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Show warning if any uninstall failed (but don't error out during uninstall)
|
||||
if [ "$uninstall_failed" = true ]; then
|
||||
warn "Failed to uninstall @aikidosec/safe-chain from some nvm Node versions"
|
||||
warn "You may need to manually run: nvm exec <version> npm uninstall -g @aikidosec/safe-chain"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main uninstallation
|
||||
main() {
|
||||
SAFE_CHAIN_LOCATION="$INSTALL_DIR/safe-chain"
|
||||
|
|
@ -89,8 +151,10 @@ main() {
|
|||
warn "safe-chain command not found. Proceeding with uninstallation."
|
||||
fi
|
||||
|
||||
# Check for existing safe-chain installation through nvm, volta, or npm
|
||||
remove_npm_installation
|
||||
remove_volta_installation
|
||||
remove_nvm_installation
|
||||
|
||||
# Remove install dir recursively if it exists
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@
|
|||
import chalk from "chalk";
|
||||
import { ui } from "../src/environment/userInteraction.js";
|
||||
import { setup } from "../src/shell-integration/setup.js";
|
||||
import { teardown, teardownDirectories } from "../src/shell-integration/teardown.js";
|
||||
import {
|
||||
teardown,
|
||||
teardownDirectories,
|
||||
} from "../src/shell-integration/teardown.js";
|
||||
import { setupCi } from "../src/shell-integration/setup-ci.js";
|
||||
import { initializeCliArguments } from "../src/config/cliArguments.js";
|
||||
import { setEcoSystem } from "../src/config/settings.js";
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { getEcoSystem } from "./settings.js";
|
|||
* @property {unknown | Number} scanTimeout
|
||||
* @property {unknown | Number} minimumPackageAgeHours
|
||||
* @property {unknown | SafeChainRegistryConfiguration} npm
|
||||
* @property {unknown | SafeChainRegistryConfiguration} pip
|
||||
*
|
||||
* @typedef {Object} SafeChainRegistryConfiguration
|
||||
* We cannot trust the input and should add the necessary validations.
|
||||
|
|
@ -104,6 +105,28 @@ export function getNpmCustomRegistries() {
|
|||
return customRegistries.filter((item) => typeof item === "string");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom npm registries from the config file (format parsing only, no validation)
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getPipCustomRegistries() {
|
||||
const config = readConfigFile();
|
||||
|
||||
if (!config || !config.pip) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TypeScript needs help understanding that config.pip exists and has customRegistries
|
||||
const pipConfig = /** @type {SafeChainRegistryConfiguration} */ (config.pip);
|
||||
const customRegistries = pipConfig.customRegistries;
|
||||
|
||||
if (!Array.isArray(customRegistries)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return customRegistries.filter((item) => typeof item === "string");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../api/aikido.js").MalwarePackage[]} data
|
||||
* @param {string | number} version
|
||||
|
|
@ -169,6 +192,9 @@ function readConfigFile() {
|
|||
npm: {
|
||||
customRegistries: undefined,
|
||||
},
|
||||
pip: {
|
||||
customRegistries: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const configFilePath = getConfigFilePath();
|
||||
|
|
|
|||
|
|
@ -232,9 +232,22 @@ describe("getMinimumPackageAgeHours", async () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("getNpmCustomRegistries", async () => {
|
||||
const { getNpmCustomRegistries } = await import("./configFile.js");
|
||||
const { getNpmCustomRegistries, getPipCustomRegistries } = await import(
|
||||
"./configFile.js"
|
||||
);
|
||||
|
||||
for (const { packageManager, getCustomRegistries } of [
|
||||
{
|
||||
packageManager: "npm",
|
||||
getCustomRegistries: getNpmCustomRegistries,
|
||||
},
|
||||
{
|
||||
packageManager: "pip",
|
||||
getCustomRegistries: getPipCustomRegistries,
|
||||
},
|
||||
])
|
||||
{
|
||||
describe(getCustomRegistries.name, async () => {
|
||||
afterEach(() => {
|
||||
configFileContent = undefined;
|
||||
});
|
||||
|
|
@ -242,49 +255,49 @@ describe("getNpmCustomRegistries", async () => {
|
|||
it("should return empty array when config file doesn't exist", () => {
|
||||
configFileContent = undefined;
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, []);
|
||||
});
|
||||
|
||||
it("should return empty array when npm config is not set", () => {
|
||||
it(`should return empty array when ${packageManager} config is not set`, () => {
|
||||
configFileContent = JSON.stringify({ scanTimeout: 5000 });
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, []);
|
||||
});
|
||||
|
||||
it("should return empty array when customRegistries is not an array", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: { customRegistries: "not-an-array" },
|
||||
[packageManager]: { customRegistries: "not-an-array" },
|
||||
});
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, []);
|
||||
});
|
||||
|
||||
it("should return array of custom registries when set", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
customRegistries: ["npm.company.com", "registry.internal.net"],
|
||||
[packageManager]: {
|
||||
customRegistries: [`${packageManager}.company.com`, "registry.internal.net"],
|
||||
},
|
||||
});
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"npm.company.com",
|
||||
`${packageManager}.company.com`,
|
||||
"registry.internal.net",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should filter out non-string values", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
[packageManager]: {
|
||||
customRegistries: [
|
||||
"npm.company.com",
|
||||
`${packageManager}.company.com`,
|
||||
123,
|
||||
null,
|
||||
"registry.internal.net",
|
||||
|
|
@ -294,20 +307,20 @@ describe("getNpmCustomRegistries", async () => {
|
|||
},
|
||||
});
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"npm.company.com",
|
||||
`${packageManager}.company.com`,
|
||||
"registry.internal.net",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return empty array for empty customRegistries array", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: { customRegistries: [] },
|
||||
[packageManager]: { customRegistries: [] },
|
||||
});
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, []);
|
||||
});
|
||||
|
|
@ -315,8 +328,9 @@ describe("getNpmCustomRegistries", async () => {
|
|||
it("should handle malformed JSON and return empty array", () => {
|
||||
configFileContent = "{ invalid json";
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,3 +15,22 @@ export function getMinimumPackageAgeHours() {
|
|||
export function getNpmCustomRegistries() {
|
||||
return process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom pip registries from environment variable
|
||||
* Expected format: comma-separated list of registry domains
|
||||
* Example: "pip.company.com,registry.internal.net"
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getPipCustomRegistries() {
|
||||
return process.env.SAFE_CHAIN_PIP_CUSTOM_REGISTRIES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the logging level from environment variable
|
||||
* Valid values: "silent", "normal", "verbose"
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getLoggingLevel() {
|
||||
return process.env.SAFE_CHAIN_LOGGING;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,20 @@ export const LOGGING_NORMAL = "normal";
|
|||
export const LOGGING_VERBOSE = "verbose";
|
||||
|
||||
export function getLoggingLevel() {
|
||||
const level = cliArguments.getLoggingLevel();
|
||||
|
||||
if (level === LOGGING_SILENT) {
|
||||
return LOGGING_SILENT;
|
||||
// Priority 1: CLI argument
|
||||
const cliLevel = cliArguments.getLoggingLevel();
|
||||
if (cliLevel === LOGGING_SILENT || cliLevel === LOGGING_VERBOSE) {
|
||||
return cliLevel;
|
||||
}
|
||||
if (cliLevel) {
|
||||
// CLI arg was set but invalid, default to normal for backwards compatibility.
|
||||
return LOGGING_NORMAL;
|
||||
}
|
||||
|
||||
if (level === LOGGING_VERBOSE) {
|
||||
return LOGGING_VERBOSE;
|
||||
// Priority 2: Environment variable
|
||||
const envLevel = environmentVariables.getLoggingLevel()?.toLowerCase();
|
||||
if (envLevel === LOGGING_SILENT || envLevel === LOGGING_VERBOSE) {
|
||||
return envLevel;
|
||||
}
|
||||
|
||||
return LOGGING_NORMAL;
|
||||
|
|
@ -143,3 +149,21 @@ export function getNpmCustomRegistries() {
|
|||
// Normalize each registry (remove protocol if any)
|
||||
return uniqueRegistries.map(normalizeRegistry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom npm registries from both environment variable and config file (merged)
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getPipCustomRegistries() {
|
||||
const envRegistries = parseRegistriesFromEnv(
|
||||
environmentVariables.getPipCustomRegistries()
|
||||
);
|
||||
const configRegistries = configFile.getPipCustomRegistries();
|
||||
|
||||
// Merge both sources and remove duplicates
|
||||
const allRegistries = [...envRegistries, ...configRegistries];
|
||||
const uniqueRegistries = [...new Set(allRegistries)];
|
||||
|
||||
// Normalize each registry (remove protocol if any)
|
||||
return uniqueRegistries.map(normalizeRegistry);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,19 +11,40 @@ mock.module("fs", {
|
|||
},
|
||||
});
|
||||
|
||||
describe("getNpmCustomRegistries", async () => {
|
||||
const {
|
||||
getNpmCustomRegistries,
|
||||
getPipCustomRegistries,
|
||||
getLoggingLevel,
|
||||
LOGGING_SILENT,
|
||||
LOGGING_NORMAL,
|
||||
LOGGING_VERBOSE,
|
||||
} = await import("./settings.js");
|
||||
const { initializeCliArguments } = await import("./cliArguments.js");
|
||||
|
||||
for (const { packageManager, getCustomRegistries, envVarName } of [
|
||||
{
|
||||
packageManager: "npm",
|
||||
getCustomRegistries: getNpmCustomRegistries,
|
||||
envVarName: "SAFE_CHAIN_NPM_CUSTOM_REGISTRIES",
|
||||
},
|
||||
{
|
||||
packageManager: "pip",
|
||||
getCustomRegistries: getPipCustomRegistries,
|
||||
envVarName: "SAFE_CHAIN_PIP_CUSTOM_REGISTRIES",
|
||||
},
|
||||
]) {
|
||||
describe(getCustomRegistries.name, async () => {
|
||||
let originalEnv;
|
||||
const { getNpmCustomRegistries } = await import("./settings.js");
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
originalEnv = process.env[envVarName];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = originalEnv;
|
||||
process.env[envVarName] = originalEnv;
|
||||
} else {
|
||||
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
delete process.env[envVarName];
|
||||
}
|
||||
configFileContent = undefined;
|
||||
});
|
||||
|
|
@ -31,77 +52,80 @@ describe("getNpmCustomRegistries", async () => {
|
|||
it("should return empty array when no registries configured", () => {
|
||||
configFileContent = undefined;
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, []);
|
||||
});
|
||||
|
||||
it("should return registries without protocol", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
customRegistries: ["npm.company.com", "registry.internal.net"],
|
||||
[packageManager]: {
|
||||
customRegistries: [
|
||||
`${packageManager}.company.com`,
|
||||
"registry.internal.net",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"npm.company.com",
|
||||
`${packageManager}.company.com`,
|
||||
"registry.internal.net",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should strip https:// protocol from registries", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
[packageManager]: {
|
||||
customRegistries: [
|
||||
"https://npm.company.com",
|
||||
`https://${packageManager}.company.com`,
|
||||
"https://registry.internal.net",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"npm.company.com",
|
||||
`${packageManager}.company.com`,
|
||||
"registry.internal.net",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should strip http:// protocol from registries", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
[packageManager]: {
|
||||
customRegistries: [
|
||||
"http://npm.company.com",
|
||||
`http://${packageManager}.company.com`,
|
||||
"http://registry.internal.net",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"npm.company.com",
|
||||
`${packageManager}.company.com`,
|
||||
"registry.internal.net",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle mixed protocols and no protocol", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
[packageManager]: {
|
||||
customRegistries: [
|
||||
"https://npm.company.com",
|
||||
`https://${packageManager}.company.com`,
|
||||
"registry.internal.net",
|
||||
"http://private.registry.io",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"npm.company.com",
|
||||
`${packageManager}.company.com`,
|
||||
"registry.internal.net",
|
||||
"private.registry.io",
|
||||
]);
|
||||
|
|
@ -109,29 +133,28 @@ describe("getNpmCustomRegistries", async () => {
|
|||
|
||||
it("should preserve registry path after stripping protocol", () => {
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
[packageManager]: {
|
||||
customRegistries: [
|
||||
"https://npm.company.com/custom/path",
|
||||
"registry.internal.net/npm",
|
||||
`https://${packageManager}.company.com/custom/path`,
|
||||
`registry.internal.net/${packageManager}`,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"npm.company.com/custom/path",
|
||||
"registry.internal.net/npm",
|
||||
`${packageManager}.company.com/custom/path`,
|
||||
`registry.internal.net/${packageManager}`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse comma-separated registries from environment variable", () => {
|
||||
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES =
|
||||
"env1.registry.com,env2.registry.net";
|
||||
delete process.env[envVarName];
|
||||
process.env[envVarName] = "env1.registry.com,env2.registry.net";
|
||||
configFileContent = undefined;
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"env1.registry.com",
|
||||
|
|
@ -140,12 +163,11 @@ describe("getNpmCustomRegistries", async () => {
|
|||
});
|
||||
|
||||
it("should trim whitespace from environment variable registries", () => {
|
||||
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES =
|
||||
" env1.registry.com , env2.registry.net ";
|
||||
delete process.env[envVarName];
|
||||
process.env[envVarName] = " env1.registry.com , env2.registry.net ";
|
||||
configFileContent = undefined;
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"env1.registry.com",
|
||||
|
|
@ -154,15 +176,15 @@ describe("getNpmCustomRegistries", async () => {
|
|||
});
|
||||
|
||||
it("should merge environment variable and config file registries", () => {
|
||||
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = "env1.registry.com";
|
||||
delete process.env[envVarName];
|
||||
process.env[envVarName] = "env1.registry.com";
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
[packageManager]: {
|
||||
customRegistries: ["config1.registry.net"],
|
||||
},
|
||||
});
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"env1.registry.com",
|
||||
|
|
@ -171,31 +193,35 @@ describe("getNpmCustomRegistries", async () => {
|
|||
});
|
||||
|
||||
it("should remove duplicate registries when merging env and config", () => {
|
||||
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES =
|
||||
"npm.company.com,env.registry.com";
|
||||
delete process.env[envVarName];
|
||||
process.env[
|
||||
envVarName
|
||||
] = `${packageManager}.company.com,env.registry.com`;
|
||||
configFileContent = JSON.stringify({
|
||||
npm: {
|
||||
customRegistries: ["npm.company.com", "config.registry.net"],
|
||||
[packageManager]: {
|
||||
customRegistries: [
|
||||
`${packageManager}.company.com`,
|
||||
"config.registry.net",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"npm.company.com",
|
||||
`${packageManager}.company.com`,
|
||||
"env.registry.com",
|
||||
"config.registry.net",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should normalize protocols from environment variable registries", () => {
|
||||
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES =
|
||||
delete process.env[envVarName];
|
||||
process.env[envVarName] =
|
||||
"https://env1.registry.com,http://env2.registry.net";
|
||||
configFileContent = undefined;
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"env1.registry.com",
|
||||
|
|
@ -204,12 +230,11 @@ describe("getNpmCustomRegistries", async () => {
|
|||
});
|
||||
|
||||
it("should handle empty strings in comma-separated list", () => {
|
||||
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES =
|
||||
"env1.registry.com,,env2.registry.net,";
|
||||
delete process.env[envVarName];
|
||||
process.env[envVarName] = "env1.registry.com,,env2.registry.net,";
|
||||
configFileContent = undefined;
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, [
|
||||
"env1.registry.com",
|
||||
|
|
@ -218,32 +243,125 @@ describe("getNpmCustomRegistries", async () => {
|
|||
});
|
||||
|
||||
it("should handle single registry in environment variable", () => {
|
||||
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = "single.registry.com";
|
||||
delete process.env[envVarName];
|
||||
process.env[envVarName] = "single.registry.com";
|
||||
configFileContent = undefined;
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, ["single.registry.com"]);
|
||||
});
|
||||
|
||||
it("should return empty array for empty environment variable", () => {
|
||||
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = "";
|
||||
delete process.env[envVarName];
|
||||
process.env[envVarName] = "";
|
||||
configFileContent = undefined;
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, []);
|
||||
});
|
||||
|
||||
it("should return empty array for whitespace-only environment variable", () => {
|
||||
delete process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||
process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES = " , , ";
|
||||
delete process.env[envVarName];
|
||||
process.env[envVarName] = " , , ";
|
||||
configFileContent = undefined;
|
||||
|
||||
const registries = getNpmCustomRegistries();
|
||||
const registries = getCustomRegistries();
|
||||
|
||||
assert.deepStrictEqual(registries, []);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("getLoggingLevel", () => {
|
||||
let originalEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.SAFE_CHAIN_LOGGING;
|
||||
delete process.env.SAFE_CHAIN_LOGGING;
|
||||
// Reset CLI arguments state
|
||||
initializeCliArguments([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.SAFE_CHAIN_LOGGING = originalEnv;
|
||||
} else {
|
||||
delete process.env.SAFE_CHAIN_LOGGING;
|
||||
}
|
||||
});
|
||||
|
||||
it("should return normal by default when nothing is configured", () => {
|
||||
const level = getLoggingLevel();
|
||||
|
||||
assert.strictEqual(level, LOGGING_NORMAL);
|
||||
});
|
||||
|
||||
it("should return silent from environment variable", () => {
|
||||
process.env.SAFE_CHAIN_LOGGING = "silent";
|
||||
|
||||
const level = getLoggingLevel();
|
||||
|
||||
assert.strictEqual(level, LOGGING_SILENT);
|
||||
});
|
||||
|
||||
it("should return verbose from environment variable", () => {
|
||||
process.env.SAFE_CHAIN_LOGGING = "verbose";
|
||||
|
||||
const level = getLoggingLevel();
|
||||
|
||||
assert.strictEqual(level, LOGGING_VERBOSE);
|
||||
});
|
||||
|
||||
it("should handle uppercase environment variable values", () => {
|
||||
process.env.SAFE_CHAIN_LOGGING = "VERBOSE";
|
||||
|
||||
const level = getLoggingLevel();
|
||||
|
||||
assert.strictEqual(level, LOGGING_VERBOSE);
|
||||
});
|
||||
|
||||
it("should handle mixed case environment variable values", () => {
|
||||
process.env.SAFE_CHAIN_LOGGING = "Silent";
|
||||
|
||||
const level = getLoggingLevel();
|
||||
|
||||
assert.strictEqual(level, LOGGING_SILENT);
|
||||
});
|
||||
|
||||
it("should return normal for invalid environment variable values", () => {
|
||||
process.env.SAFE_CHAIN_LOGGING = "invalid";
|
||||
|
||||
const level = getLoggingLevel();
|
||||
|
||||
assert.strictEqual(level, LOGGING_NORMAL);
|
||||
});
|
||||
|
||||
it("should prioritize CLI argument over environment variable", () => {
|
||||
process.env.SAFE_CHAIN_LOGGING = "verbose";
|
||||
initializeCliArguments(["--safe-chain-logging=silent"]);
|
||||
|
||||
const level = getLoggingLevel();
|
||||
|
||||
assert.strictEqual(level, LOGGING_SILENT);
|
||||
});
|
||||
|
||||
it("should use environment variable when CLI argument is not set", () => {
|
||||
process.env.SAFE_CHAIN_LOGGING = "silent";
|
||||
initializeCliArguments(["install", "express"]);
|
||||
|
||||
const level = getLoggingLevel();
|
||||
|
||||
assert.strictEqual(level, LOGGING_SILENT);
|
||||
});
|
||||
|
||||
it("should return normal when CLI argument is invalid (even if env var is valid)", () => {
|
||||
process.env.SAFE_CHAIN_LOGGING = "verbose";
|
||||
initializeCliArguments(["--safe-chain-logging=invalid"]);
|
||||
|
||||
const level = getLoggingLevel();
|
||||
|
||||
assert.strictEqual(level, LOGGING_NORMAL);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ import { getAuditStats } from "./scanning/audit/index.js";
|
|||
* @returns {Promise<number>}
|
||||
*/
|
||||
export async function main(args) {
|
||||
if (isSafeChainVerify(args)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
process.on("SIGINT", handleProcessTermination);
|
||||
process.on("SIGTERM", handleProcessTermination);
|
||||
|
||||
|
|
@ -104,3 +108,12 @@ export async function main(args) {
|
|||
function handleProcessTermination() {
|
||||
ui.writeBufferedLogsAndStopBuffering();
|
||||
}
|
||||
|
||||
/** @param {string[]} args */
|
||||
function isSafeChainVerify(args) {
|
||||
const safeChainCheckCommand = "safe-chain-verify";
|
||||
if (args.length > 0 && args[0] === safeChainCheckCommand) {
|
||||
ui.writeInformation("OK: Safe-chain works!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { getPipCustomRegistries } from "../../config/settings.js";
|
||||
import { isMalwarePackage } from "../../scanning/audit/index.js";
|
||||
import { interceptRequests } from "./interceptorBuilder.js";
|
||||
|
||||
|
|
@ -13,7 +14,9 @@ const knownPipRegistries = [
|
|||
* @returns {import("./interceptorBuilder.js").Interceptor | undefined}
|
||||
*/
|
||||
export function pipInterceptorForUrl(url) {
|
||||
const registry = knownPipRegistries.find((reg) => url.includes(reg));
|
||||
const customRegistries = getPipCustomRegistries();
|
||||
const registries = [...knownPipRegistries, ...customRegistries];
|
||||
const registry = registries.find((reg) => url.includes(reg));
|
||||
|
||||
if (registry) {
|
||||
return buildPipInterceptor(registry);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
import { describe, it, mock } from "node:test";
|
||||
import assert from "node:assert";
|
||||
|
||||
describe("pipInterceptor custom registries", async () => {
|
||||
let lastPackage;
|
||||
let malwareResponse = false;
|
||||
let customRegistries = [];
|
||||
|
||||
mock.module("../../config/settings.js", {
|
||||
namedExports: {
|
||||
getPipCustomRegistries: () => customRegistries,
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("../../scanning/audit/index.js", {
|
||||
namedExports: {
|
||||
isMalwarePackage: async (packageName, version) => {
|
||||
lastPackage = { packageName, version };
|
||||
return malwareResponse;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { pipInterceptorForUrl } = await import("./pipInterceptor.js");
|
||||
|
||||
it("should create interceptor for custom registry", () => {
|
||||
customRegistries = ["my-custom-registry.example.com"];
|
||||
const url =
|
||||
"https://my-custom-registry.example.com/packages/xx/yy/foobar-1.2.3.tar.gz";
|
||||
|
||||
const interceptor = pipInterceptorForUrl(url);
|
||||
|
||||
assert.ok(
|
||||
interceptor,
|
||||
"Interceptor should be created for custom registry"
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse package from custom registry URL", async () => {
|
||||
customRegistries = ["my-custom-registry.example.com"];
|
||||
const url =
|
||||
"https://my-custom-registry.example.com/packages/xx/yy/foobar-1.2.3.tar.gz";
|
||||
|
||||
const interceptor = pipInterceptorForUrl(url);
|
||||
assert.ok(interceptor, "Interceptor should be created");
|
||||
|
||||
await interceptor.handleRequest(url);
|
||||
|
||||
assert.deepEqual(lastPackage, {
|
||||
packageName: "foobar",
|
||||
version: "1.2.3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse wheel package from custom registry URL", async () => {
|
||||
customRegistries = ["private-pypi.internal.com"];
|
||||
const url =
|
||||
"https://private-pypi.internal.com/packages/foo_bar-2.0.0-py3-none-any.whl";
|
||||
|
||||
const interceptor = pipInterceptorForUrl(url);
|
||||
assert.ok(interceptor, "Interceptor should be created");
|
||||
|
||||
await interceptor.handleRequest(url);
|
||||
|
||||
assert.deepEqual(lastPackage, {
|
||||
packageName: "foo-bar",
|
||||
version: "2.0.0",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle multiple custom registries", async () => {
|
||||
customRegistries = [
|
||||
"registry-one.example.com",
|
||||
"registry-two.example.com",
|
||||
];
|
||||
|
||||
const url1 =
|
||||
"https://registry-one.example.com/packages/package1-1.0.0.tar.gz";
|
||||
const url2 =
|
||||
"https://registry-two.example.com/packages/package2-2.0.0.tar.gz";
|
||||
|
||||
const interceptor1 = pipInterceptorForUrl(url1);
|
||||
const interceptor2 = pipInterceptorForUrl(url2);
|
||||
|
||||
assert.ok(interceptor1, "Interceptor should be created for first registry");
|
||||
assert.ok(
|
||||
interceptor2,
|
||||
"Interceptor should be created for second registry"
|
||||
);
|
||||
});
|
||||
|
||||
it("should block malicious package from custom registry", async () => {
|
||||
customRegistries = ["my-custom-registry.example.com"];
|
||||
malwareResponse = true;
|
||||
|
||||
const url =
|
||||
"https://my-custom-registry.example.com/packages/malicious_package-1.0.0.tar.gz";
|
||||
|
||||
const interceptor = pipInterceptorForUrl(url);
|
||||
assert.ok(interceptor, "Interceptor should be created");
|
||||
|
||||
const result = await interceptor.handleRequest(url);
|
||||
|
||||
assert.ok(result.blockResponse, "Should contain a blockResponse");
|
||||
assert.equal(
|
||||
result.blockResponse.statusCode,
|
||||
403,
|
||||
"Block response should have status code 403"
|
||||
);
|
||||
assert.equal(
|
||||
result.blockResponse.message,
|
||||
"Forbidden - blocked by safe-chain",
|
||||
"Block response should have correct status message"
|
||||
);
|
||||
|
||||
malwareResponse = false;
|
||||
});
|
||||
|
||||
it("should still work with known registries when custom registries are set", async () => {
|
||||
customRegistries = ["my-custom-registry.example.com"];
|
||||
|
||||
const url =
|
||||
"https://files.pythonhosted.org/packages/xx/yy/foobar-1.2.3.tar.gz";
|
||||
|
||||
const interceptor = pipInterceptorForUrl(url);
|
||||
|
||||
assert.ok(
|
||||
interceptor,
|
||||
"Interceptor should be created for known registry even with custom registries set"
|
||||
);
|
||||
|
||||
await interceptor.handleRequest(url);
|
||||
|
||||
assert.deepEqual(lastPackage, {
|
||||
packageName: "foobar",
|
||||
version: "1.2.3",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not create interceptor for unknown registry when custom registries are set", () => {
|
||||
customRegistries = ["my-custom-registry.example.com"];
|
||||
const url = "https://unknown-registry.example.com/packages/foobar-1.0.0.tar.gz";
|
||||
|
||||
const interceptor = pipInterceptorForUrl(url);
|
||||
|
||||
assert.equal(
|
||||
interceptor,
|
||||
undefined,
|
||||
"Interceptor should be undefined for unknown registry"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle empty custom registries array", () => {
|
||||
customRegistries = [];
|
||||
const url =
|
||||
"https://my-custom-registry.example.com/packages/foobar-1.0.0.tar.gz";
|
||||
|
||||
const interceptor = pipInterceptorForUrl(url);
|
||||
|
||||
assert.equal(
|
||||
interceptor,
|
||||
undefined,
|
||||
"Interceptor should be undefined when no custom registries are configured"
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse .whl.metadata from custom registry", async () => {
|
||||
customRegistries = ["private-pypi.internal.com"];
|
||||
const url =
|
||||
"https://private-pypi.internal.com/packages/foo_bar-2.0.0-py3-none-any.whl.metadata";
|
||||
|
||||
const interceptor = pipInterceptorForUrl(url);
|
||||
assert.ok(interceptor, "Interceptor should be created");
|
||||
|
||||
await interceptor.handleRequest(url);
|
||||
|
||||
assert.deepEqual(lastPackage, {
|
||||
packageName: "foo-bar",
|
||||
version: "2.0.0",
|
||||
});
|
||||
});
|
||||
|
||||
it("should parse .tar.gz.metadata from custom registry", async () => {
|
||||
customRegistries = ["private-pypi.internal.com"];
|
||||
const url =
|
||||
"https://private-pypi.internal.com/packages/foo_bar-2.0.0.tar.gz.metadata";
|
||||
|
||||
const interceptor = pipInterceptorForUrl(url);
|
||||
assert.ok(interceptor, "Interceptor should be created");
|
||||
|
||||
await interceptor.handleRequest(url);
|
||||
|
||||
assert.deepEqual(lastPackage, {
|
||||
packageName: "foo-bar",
|
||||
version: "2.0.0",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -83,6 +83,6 @@ function wrapSafeChainCommand() {
|
|||
# If the aikido command is not available, print a warning and run the original command
|
||||
printSafeChainWarning "$original_cmd"
|
||||
|
||||
command "$original_cmd" "$@"
|
||||
command "$@"
|
||||
fi
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue