Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/verify-npm-security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Verify NPM Security

on:
workflow_call:
inputs:
working-directory:
description: "Working directory"
required: true
type: string

jobs:
verify-npm-install:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Log in to GitHub Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

- name: Pull container
run: docker pull ghcr.io/fundwave/npm-security:latest

- name: Verify npm install
working-directory: ${{ inputs.working-directory }}
run: |
docker run --rm \
-v $(pwd):/app \
-e GITHUB_READ_TOKEN=${{ secrets.GITHUB_TOKEN }} \
ghcr.io/fundwave/npm-security:latest

- name: Verification complete
run: echo "NPM dependencies verified successfully in ${{ inputs.working-directory }}"
3 changes: 3 additions & 0 deletions npm-security/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//npm.pkg.github.com/:_authToken=${GITHUB_READ_TOKEN}
@fundwave:registry=https://npm.pkg.github.com
legacy-peer-deps=true
14 changes: 14 additions & 0 deletions npm-security/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM node:22-alpine

RUN mkdir -p /home/node/test
RUN chown -R node:node /home/node/test

WORKDIR /home/node/test

COPY .npmrc /home/node/.npmrc
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

USER node

ENTRYPOINT ["sh", "/entrypoint.sh"]
126 changes: 126 additions & 0 deletions npm-security/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
WARNING: Only use read-only GITHUB token as GITHUB_READ_TOKEN. If a WRITE token is supplied, the container will cause malicious packages to be published.

# NPM Security Container

A Docker container designed to securely install npm dependencies and perform integrity checks on JavaScript files looking specifically for hashes that indicate infiltration by Sha1-Hulud malware that came out on Nov 24 2025. This container can be used as a precautionary step before `npm install`.

## Overview

This container performs the following operations:
1. Sets up authentication for GitHub packages (if `GITHUB_READ_TOKEN` is provided)
2. Runs `npm install` to install dependencies
3. Performs integrity checks on JavaScript files using SHA256 hashes that point to Sha1-Hulud malware files.
4. Returns appropriate exit codes based on the results

## Exit Codes

The container uses specific exit codes to indicate different states:

- **Exit Code 0**: ✅ **Success** - npm install succeeded and all integrity checks passed
- **Exit Code 1**: ❌ **Security Alert** - Integrity check failed, potentially malicious files detected
- **Exit Code 2**: 🔧 **Installation Error** - npm install failed

## Usage

### Basic Usage

```bash
docker pull ghcr.io/fundwave/npm-security:latest
docker run --rm -v $(pwd):/app ghcr.io/fundwave/npm-security:latest
```

### With GitHub Read Token (for private packages)

```bash
docker run --rm -v $(pwd):/app -e GITHUB_READ_TOKEN=your_github_readonly_token ghcr.io/fundwave/npm-security:latest
```

### CI/CD Integration

In your CI/CD pipeline, you can use the exit codes to determine the next steps:

```yaml
# Example GitHub Actions workflow
- name: Secure NPM Install
run: |
docker run --rm -v $(pwd):/app -e GITHUB_READ_TOKEN=${{ secrets.GITHUB_TOKEN }} ghcr.io/fundwave/npm-security:latest
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo "Proceeding with build..."
elif [ $EXIT_CODE -eq 1 ]; then
echo "Security check failed - stopping deployment"
exit 1
elif [ $EXIT_CODE -eq 2 ]; then
echo "Installation failed - check dependencies"
exit 1
fi
```

## Security Features

### Integrity Checking

The container checks for known malicious file hashes:
- Scans all `.js` files in the project
- Compares SHA256 hashes against a database of known threats
- Fails if any malicious patterns are detected

### Isolated Environment

- Runs in a containerized environment
- No access to host system beyond mounted volume
- Clean Alpine Linux base with minimal attack surface
- Use only a read-only GITHUB_TOKEN (with package read permissions only). Do not use tokens with write or publish permissions, as this may allow malicious packages to be published.

### Authentication

- Supports GitHub Package Registry authentication
- Tokens are handled securely through environment variables
- No credentials stored in the container image

## Development

### Building the Container

```bash
docker build -t npm-security .
```

### Testing

Test with a clean Node.js project:

```bash
# Create test project
mkdir test-project && cd test-project
npm init -y
echo '{}' > package.json

# Test the container
docker run --rm -v $(pwd):/app npm-security
echo "Exit code: $?"
```

## Environment Variables

- `GITHUB_READ_TOKEN`: GitHub Personal Access Token for accessing private packages in GitHub Package Registry

## Troubleshooting

### Common Issues

1. **Permission denied**: Ensure the mounted volume has correct permissions
2. **Network issues**: Check Docker network configuration for npm registry access
3. **Token issues**: Verify GitHub token has correct permissions for package registry

### Debug Mode

Run the container interactively to debug issues:

```bash
docker run -it --rm -v $(pwd):/app --entrypoint sh npm-security
```

## Contributing

When modifying the integrity check hashes, ensure you're adding legitimate threat signatures and document the source of the hash information.
60 changes: 60 additions & 0 deletions npm-security/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/bin/sh
set -e

# Known malicious SHA256 hashes (Sha1-Hulud malware signatures)
MALICIOUS_HASHES="a3894003ad1d293ba96d77881ccd2071446dc3f65f434669b49b3da92421901a|\
62ee164b9b306250c1172583f138c9614139264f889fa99614903c12755468d0|\
c723605455e8667a4c84327cf6b704bbdcb9b4ce3707ddddd927d32b8372ff77|\
2e44e8d8a8e906fd5bfbb37be08dfe2dcf1ce41bd4ba726987ab516446dfb4f1|\
fa7df9e9fc5390cc54e0086073fc9b3054087ffddf661bbc9f836b007fa25f20|\
d66343059793800e72ef17690ce26492dc854c8513905778630ff1ed4e7a81b8|\
981d3e2f5d7e26c93bd4b758ea722468900894fb2368db5f8399282e2414fe33"

# Exit codes
EXIT_SUCCESS=0
EXIT_SECURITY_ALERT=1
EXIT_INSTALLATION_ERROR=2

echo "Validating GitHub Token..."

if [ -f /app/package.json ] ; then cp /app/package.json ~/test/package.json; fi
if [ -f /app/package-lock.json ] ; then cp /app/package-lock.json ~/test/package-lock.json; fi
Comment on lines +20 to +21
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The working directory is set to /home/node/test in the Dockerfile, but these commands copy to ~/test/. Since the script runs as the 'node' user, ~ expands to /home/node/, making the destination /home/node/test/. However, the source path /app/ doesn't align with the /home/node/test working directory. The volume mount is expected at /app, but the script should work within the current directory. Consider using cp /app/package*.json . or clarifying the directory structure.

Suggested change
if [ -f /app/package.json ] ; then cp /app/package.json ~/test/package.json; fi
if [ -f /app/package-lock.json ] ; then cp /app/package-lock.json ~/test/package-lock.json; fi
if [ -f /app/package.json ] ; then cp /app/package.json ./package.json; fi
if [ -f /app/package-lock.json ] ; then cp /app/package-lock.json ./package-lock.json; fi

Copilot uses AI. Check for mistakes.
if [ -f /.env ] ; then echo ".env file found. Exiting"; exit $EXIT_INSTALLATION_ERROR; fi
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path /.env checks for a .env file in the root directory, but based on the volume mount at /app and working directory structure, this should likely be /app/.env or ./.env to check in the project directory. The current path would only catch a .env file in the container's root filesystem.

Suggested change
if [ -f /.env ] ; then echo ".env file found. Exiting"; exit $EXIT_INSTALLATION_ERROR; fi
if [ -f /app/.env ] ; then echo ".env file found. Exiting"; exit $EXIT_INSTALLATION_ERROR; fi

Copilot uses AI. Check for mistakes.

# Skip validation if no token provided
if [ -z "${GITHUB_READ_TOKEN}" ]; then
echo "No GITHUB_READ_TOKEN provided, skipping token validation."
else

# Fetch GitHub API headers to validate token
GITHUB_TOKEN_HEADERS=$(wget --server-response --spider \
--header="Authorization: token ${GITHUB_READ_TOKEN}" \
https://api.github.com 2>&1)

if echo "${GITHUB_READ_TOKEN}" | grep -q "^ghs_"; then
echo "GitHub Actions token detected - skipping read-only scope check."
elif ! echo "$GITHUB_TOKEN_HEADERS" | grep -qi 'x-oauth-scopes:'; then
echo "ERROR: Cannot determine token scope (may be fine-grained). Only read-only tokens are permitted!"
exit $EXIT_INSTALLATION_ERROR
elif echo "$GITHUB_TOKEN_HEADERS" | grep -i 'x-oauth-scopes:' | grep -qi 'write'; then
echo "ERROR: Token has write access. Only read-only tokens are permitted!"
exit $EXIT_INSTALLATION_ERROR
else
echo "Token validation passed - read-only access confirmed."
fi
fi

echo "Running npm install..."
npm install || { echo "ERROR: npm install failed!"; exit $EXIT_INSTALLATION_ERROR; }

echo "Performing integrity checks on JavaScript files..."
SUSPICIOUS_FILES=$(find . -type f -name "*.js" -exec sha256sum {} \; | grep -iE "$MALICIOUS_HASHES" || true)

# Exit if malicious files found
[ -n "$SUSPICIOUS_FILES" ] && {
echo "SECURITY ALERT: Malicious files detected!"
echo "$SUSPICIOUS_FILES"
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The suspicious files output should be formatted for better readability and actionability. Consider iterating over the results to show the file path and hash separately, making it easier to identify which specific files triggered the alert.

Suggested change
echo "$SUSPICIOUS_FILES"
echo "The following suspicious files were found:"
echo "$SUSPICIOUS_FILES" | while read -r line; do
HASH=$(echo "$line" | awk '{print $1}')
FILE=$(echo "$line" | awk '{print $2}')
echo " File: $FILE"
echo " Hash: $HASH"
echo "-----------------------------"
done

Copilot uses AI. Check for mistakes.
exit $EXIT_SECURITY_ALERT
}

exit $EXIT_SUCCESS