Sandbox architecture
Defense-in-depth for executing and analyzing untrusted packages safely.
Runtime Sandbox Architecture
Overview
This document describes the architecture for a secure runtime analysis sandbox that executes npm packages to detect malicious behavior that cannot be caught by static analysis alone.
Key Principle: Defense in depth with multiple isolation layers. The package under test should never be able to:
- Exfiltrate data to the internet
- Access or contaminate our detection tools
- Persist beyond the analysis session
- Affect other analyses (no cross-contamination)
Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────────┐
│ AWS Lambda Function │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Orchestrator Container │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Detection Tools Layer │ │ │
│ │ │ • Network Monitor (eBPF/strace) │ │ │
│ │ │ • File System Monitor │ │ │
│ │ │ • Process Monitor │ │ │
│ │ │ • Resource Monitor │ │ │
│ │ │ • JSON Report Generator │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ (read-only observation) │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Inner Sandbox (gVisor/nsjail) │ │ │
│ │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Package Under Test │ │ │ │
│ │ │ │ • npm install <package> │ │ │ │
│ │ │ │ • Run test suite │ │ │ │
│ │ │ │ • Execute dummy project │ │ │ │
│ │ │ └───────────────────────────────────────────────────────────┘ │ │ │
│ │ │ Restrictions: │ │ │
│ │ │ • No network (loopback only) │ │ │
│ │ │ • Read-only root filesystem │ │ │
│ │ │ • Writable /tmp only (size limited) │ │ │
│ │ │ • No access to host processes │ │ │
│ │ │ • Syscall filtering (seccomp) │ │ │
│ │ │ • Resource limits (cgroups) │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ VPC Configuration: │
│ • Private subnet (no internet gateway) │
│ • No NAT gateway │
│ • S3 VPC endpoint only (for result upload) │
└─────────────────────────────────────────────────────────────────────────────┘
Security Layers
Layer 1: AWS Lambda + VPC Isolation
Purpose: Network isolation and ephemeral execution environment
Configuration:
resource "aws_lambda_function" "sandbox" {
function_name = "malware-scanner-sandbox"
package_type = "Image"
image_uri = "${aws_ecr_repository.sandbox.repository_url}:latest"
timeout = 900 # 15 minutes max
memory_size = 3008 # MB
vpc_config {
subnet_ids = [aws_subnet.private_sandbox.id]
security_group_ids = [aws_security_group.sandbox_no_egress.id]
}
# No environment variables with secrets
environment {
variables = {
SANDBOX_MODE = "strict"
}
}
}
# Security group with NO egress
resource "aws_security_group" "sandbox_no_egress" {
name = "sandbox-no-egress"
description = "No egress allowed - complete network isolation"
vpc_id = aws_vpc.sandbox.id
# No ingress rules
# No egress rules - complete isolation
}
# VPC endpoint for S3 only (result upload)
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.sandbox.id
service_name = "com.amazonaws.${var.region}.s3"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = "*"
Action = ["s3:PutObject"]
Resource = "${aws_s3_bucket.results.arn}/*"
}]
})
}
Security Properties:
- No internet access (no NAT gateway, no IGW)
- Only S3 VPC endpoint for uploading results
- Ephemeral execution (Lambda destroys environment after each invocation)
- 15-minute maximum execution time
- Memory/CPU limits enforced by Lambda
Layer 2: Orchestrator Container
Purpose: Run detection tools in isolation from the package under test
Dockerfile:
FROM public.ecr.aws/lambda/nodejs:20
# Install sandboxing tools
RUN yum install -y \
strace \
inotify-tools \
procps-ng \
&& yum clean all
# Install nsjail for inner sandbox
COPY --from=nsjail-builder /nsjail /usr/local/bin/nsjail
# Detection tools (read-only, separate from package)
COPY detection-tools/ /opt/detection/
RUN chmod -R 555 /opt/detection/
# Sandbox configuration
COPY sandbox-config/ /opt/sandbox-config/
RUN chmod -R 444 /opt/sandbox-config/
# Lambda handler
COPY handler.js ${LAMBDA_TASK_ROOT}/
CMD ["handler.analyze"]
Responsibilities:
- Receives package name/version to analyze
- Downloads package tarball (during build or via S3)
- Sets up inner sandbox with package
- Starts monitoring tools BEFORE package execution
- Collects all observations into JSON
- Uploads results to S3
- Never executes package code directly
Layer 3: Inner Sandbox (nsjail/gVisor)
Purpose: Execute the package in a completely isolated environment
nsjail Configuration:
# /opt/sandbox-config/nsjail.cfg
name: "npm-package-sandbox"
mode: ONCE
time_limit: 300 # 5 minutes max
# Namespaces
clone_newnet: true # Isolated network namespace
clone_newuser: true # Isolated user namespace
clone_newns: true # Isolated mount namespace
clone_newpid: true # Isolated PID namespace
clone_newipc: true # Isolated IPC namespace
clone_newuts: true # Isolated UTS namespace
clone_newcgroup: true # Isolated cgroup namespace
# User mapping (run as nobody)
uidmap {
inside_id: "65534"
outside_id: "65534"
count: 1
}
gidmap {
inside_id: "65534"
outside_id: "65534"
count: 1
}
# Mount configuration
mount {
src: "/opt/sandbox-rootfs"
dst: "/"
is_bind: true
rw: false
}
mount {
dst: "/tmp"
fstype: "tmpfs"
rw: true
options: "size=100M,noexec,nosuid"
}
mount {
dst: "/proc"
fstype: "proc"
rw: false
}
# Resource limits
rlimit_as_type: SOFT # Address space
rlimit_as: 512 # 512 MB
rlimit_cpu_type: SOFT
rlimit_cpu: 300 # 5 minutes CPU
rlimit_fsize_type: SOFT
rlimit_fsize: 50 # 50 MB max file size
rlimit_nofile_type: SOFT
rlimit_nofile: 256 # Max open files
rlimit_nproc_type: SOFT
rlimit_nproc: 32 # Max processes
# Seccomp - block dangerous syscalls
seccomp_policy_file: "/opt/sandbox-config/seccomp.policy"
# cgroup limits
cgroup_mem_max: 512000000 # 512 MB
cgroup_pids_max: 64 # Max 64 processes
cgroup_cpu_ms_per_sec: 800 # 80% CPU max
# No network access (loopback only)
# Network namespace is isolated, no interfaces added
# Capabilities - drop everything
cap {
# Empty - no capabilities
}
Seccomp Policy (allowlist):
# /opt/sandbox-config/seccomp.policy
# Only allow safe syscalls
ALLOW {
read, write, open, close, stat, fstat, lstat,
poll, lseek, mmap, mprotect, munmap, brk,
ioctl, access, pipe, select, sched_yield,
dup, dup2, nanosleep, getpid, getuid, getgid,
geteuid, getegid, getppid, getpgrp,
fcntl, flock, fsync, fdatasync, truncate,
getdents, getcwd, chdir, fchdir, readlink,
chmod, fchmod, chown, fchown, lchown,
umask, gettimeofday, getrlimit, getrusage,
times, sysinfo, uname, arch_prctl,
futex, epoll_create, epoll_ctl, epoll_wait,
clock_gettime, clock_getres,
exit, exit_group, wait4, rt_sigaction,
rt_sigprocmask, rt_sigreturn, clone, fork
}
# Block dangerous syscalls
DENY {
ptrace, # No debugging other processes
process_vm_readv, # No reading other process memory
process_vm_writev,
mount, umount2, # No mounting
reboot, kexec_load,
init_module, finit_module, delete_module,
acct, swapon, swapoff,
sethostname, setdomainname,
ioperm, iopl,
socket, connect, accept, bind, listen, # No network
sendto, recvfrom, sendmsg, recvmsg,
execveat # Only execve allowed, not execveat
}
Detection Tools Design
Tool 1: Network Monitor
Purpose: Capture all network activity attempts (even if blocked)
// /opt/detection/monitors/network-monitor.ts
interface NetworkAttempt {
timestamp: number;
pid: number;
syscall: 'socket' | 'connect' | 'sendto' | 'dns_lookup';
args: {
family?: 'AF_INET' | 'AF_INET6' | 'AF_UNIX';
address?: string;
port?: number;
hostname?: string;
};
blocked: boolean;
stackTrace?: string;
}
/**
* Uses strace to monitor network syscalls on the sandbox process
* All attempts are blocked by seccomp but we log them
*/
export class NetworkMonitor {
private attempts: NetworkAttempt[] = [];
async monitor(sandboxPid: number): Promise<NetworkAttempt[]> {
// strace -f -e trace=network -p <pid>
// Parse output and collect attempts
return this.attempts;
}
}
Tool 2: File System Monitor
Purpose: Track all file system access patterns
// /opt/detection/monitors/fs-monitor.ts
interface FileAccess {
timestamp: number;
pid: number;
operation: 'read' | 'write' | 'open' | 'stat' | 'unlink' | 'exec';
path: string;
flags?: string;
result: 'allowed' | 'denied' | 'not_found';
}
interface SensitiveFileAttempt {
path: string;
patterns: string[]; // e.g., ['.ssh', 'id_rsa', 'credentials']
severity: 'critical' | 'warning' | 'info';
}
/**
* Uses inotifywait and strace to monitor file access
*/
export class FileSystemMonitor {
private accesses: FileAccess[] = [];
private sensitiveAttempts: SensitiveFileAttempt[] = [];
// Patterns that indicate malicious intent
private sensitivePatterns = [
{ pattern: /\.ssh/, severity: 'critical' },
{ pattern: /id_rsa|id_ed25519/, severity: 'critical' },
{ pattern: /\.aws\/credentials/, severity: 'critical' },
{ pattern: /\.env/, severity: 'warning' },
{ pattern: /\/etc\/passwd/, severity: 'info' },
{ pattern: /\/etc\/shadow/, severity: 'critical' },
{ pattern: /\.npmrc/, severity: 'warning' },
{ pattern: /\.gitconfig/, severity: 'info' },
];
}
Tool 3: Process Monitor
Purpose: Track process creation and execution patterns
// /opt/detection/monitors/process-monitor.ts
interface ProcessEvent {
timestamp: number;
type: 'spawn' | 'exec' | 'exit';
pid: number;
ppid: number;
command: string;
args: string[];
exitCode?: number;
}
interface SuspiciousExecution {
command: string;
args: string[];
reason: string;
severity: 'critical' | 'warning' | 'info';
}
/**
* Monitors process tree inside sandbox
*/
export class ProcessMonitor {
// Commands that are suspicious when executed by an npm package
private suspiciousCommands = [
{ command: 'curl', severity: 'critical', reason: 'Network download attempt' },
{ command: 'wget', severity: 'critical', reason: 'Network download attempt' },
{ command: 'nc', severity: 'critical', reason: 'Netcat - potential reverse shell' },
{ command: 'bash', severity: 'warning', reason: 'Shell execution' },
{ command: 'sh', severity: 'warning', reason: 'Shell execution' },
{ command: 'python', severity: 'warning', reason: 'Python interpreter' },
{ command: 'perl', severity: 'warning', reason: 'Perl interpreter' },
{ command: 'base64', severity: 'warning', reason: 'Encoding/decoding' },
{ command: 'xxd', severity: 'warning', reason: 'Hex dump utility' },
];
}
Tool 4: Environment Monitor
Purpose: Track environment variable access
// /opt/detection/monitors/env-monitor.ts
interface EnvAccess {
timestamp: number;
pid: number;
variable: string;
operation: 'read' | 'write';
value?: string; // Only for writes, reads show attempted name
}
/**
* Injects LD_PRELOAD to intercept getenv/setenv calls
*/
export class EnvironmentMonitor {
// Sensitive env vars to track
private sensitiveVars = [
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
'GITHUB_TOKEN',
'NPM_TOKEN',
'DATABASE_URL',
'STRIPE_SECRET_KEY',
'API_KEY',
'SECRET',
'PASSWORD',
'PRIVATE_KEY',
];
}
Tool 5: Resource Monitor
Purpose: Track resource usage patterns
// /opt/detection/monitors/resource-monitor.ts
interface ResourceMetrics {
timestamp: number;
cpu_percent: number;
memory_mb: number;
disk_io_mb: number;
open_files: number;
thread_count: number;
}
interface ResourceAnomaly {
type: 'cpu_spike' | 'memory_spike' | 'crypto_mining_pattern';
details: string;
severity: 'warning' | 'critical';
}
/**
* Monitors resource usage for anomalies (e.g., crypto mining)
*/
export class ResourceMonitor {
// Detect crypto mining patterns
// - Sustained high CPU usage
// - Specific CPU instruction patterns
}
Test Execution Strategies
Strategy 1: Package Test Suite
interface TestSuiteResult {
executed: boolean;
passed: number;
failed: number;
error?: string;
suspicious_behaviors: SuspiciousBehavior[];
}
async function runPackageTests(packagePath: string): Promise<TestSuiteResult> {
// 1. Check if package has test script
// 2. Run: npm test (inside sandbox)
// 3. Monitor all system calls during execution
// 4. Collect results
}
Strategy 2: Dummy Project Execution
interface DummyProjectTemplate {
name: string;
description: string;
code: string;
expectedBehavior: string[];
}
// Templates for different package types
const templates: Record<string, DummyProjectTemplate> = {
'http-client': {
name: 'HTTP Client Test',
description: 'Tests packages that make HTTP requests',
code: `
const pkg = require('{{PACKAGE_NAME}}');
// Try basic operations - monitor what happens
if (pkg.get) pkg.get('http://localhost/test');
if (pkg.fetch) pkg.fetch('http://localhost/test');
`,
expectedBehavior: ['network_attempt'],
},
'file-util': {
name: 'File Utility Test',
description: 'Tests packages that do file operations',
code: `
const pkg = require('{{PACKAGE_NAME}}');
// Try basic operations
if (pkg.read) pkg.read('/tmp/test.txt');
if (pkg.write) pkg.write('/tmp/test.txt', 'data');
`,
expectedBehavior: ['file_read', 'file_write'],
},
'generic': {
name: 'Generic Import Test',
description: 'Just imports the package and monitors install hooks',
code: `
require('{{PACKAGE_NAME}}');
// Package loaded - monitor what happened during load
`,
expectedBehavior: [],
},
};
Strategy 3: Install Hook Monitoring
interface InstallHookResult {
preinstall: ProcessEvent[];
install: ProcessEvent[];
postinstall: ProcessEvent[];
lifecycle_scripts: {
script: string;
executed: boolean;
output: string;
suspicious: boolean;
}[];
}
async function monitorInstall(packageName: string): Promise<InstallHookResult> {
// 1. npm install <package> --ignore-scripts (get the files)
// 2. Inspect package.json for install scripts
// 3. Run each script individually with full monitoring
// 4. Capture all behavior
}
Output Format
JSON Report Schema
interface SandboxAnalysisReport {
// Metadata
metadata: {
package_name: string;
package_version: string;
analysis_id: string;
timestamp: string;
duration_ms: number;
sandbox_version: string;
};
// Installation analysis
installation: {
success: boolean;
install_scripts: {
name: string;
command: string;
executed: boolean;
exit_code: number;
duration_ms: number;
}[];
dependencies_installed: number;
};
// Runtime behavior observations
behavior: {
network_attempts: NetworkAttempt[];
file_accesses: FileAccess[];
process_events: ProcessEvent[];
env_accesses: EnvAccess[];
resource_anomalies: ResourceAnomaly[];
};
// Test execution results
tests: {
package_tests: TestSuiteResult | null;
dummy_project_tests: {
template: string;
result: DummyProjectResult;
}[];
};
// Aggregated alerts
alerts: {
type: string;
severity: 'critical' | 'warning' | 'info';
title: string;
description: string;
evidence: unknown;
}[];
// Overall assessment
assessment: {
threat_level: 'clean' | 'suspicious' | 'malicious';
confidence: number;
summary: string;
};
}
Security Guarantees
What the sandbox PREVENTS:
-
Network Exfiltration
- No network interfaces in sandbox namespace
- All socket syscalls blocked by seccomp
- Lambda in VPC with no internet gateway
- Only S3 VPC endpoint for result upload
-
File System Escape
- Read-only root filesystem
- Only /tmp writable (100MB tmpfs, noexec)
- Mount namespace isolation
- No access to host filesystem
-
Process Escape
- PID namespace isolation
- No ptrace allowed
- seccomp blocks dangerous syscalls
- cgroup process limits
-
Detection Tool Contamination
- Detection tools run OUTSIDE inner sandbox
- Only observe via strace/inotify (read-only)
- No shared memory between tools and package
- Tools have no writable paths accessible to package
-
Cross-Analysis Contamination
- Lambda creates fresh environment each invocation
- No persistent storage
- No shared state between analyses
-
Resource Exhaustion
- CPU limits via cgroups
- Memory limits (512MB)
- Time limits (5 minutes inner, 15 minutes outer)
- File size limits
- Process count limits
What the sandbox ALLOWS (for monitoring):
- Package installation (npm install)
- Test suite execution (npm test)
- Basic code execution (require/import)
- File operations within /tmp
- Environment variable reads
- Process spawning (limited count)
Implementation Phases
Phase 1: Basic Infrastructure
- Lambda function with VPC isolation
- Container image with nsjail
- Basic package installation monitoring
- S3 result upload
Phase 2: Detection Tools
- Network monitor (strace-based)
- File system monitor
- Process monitor
- Environment monitor
Phase 3: Test Execution
- Package test suite runner
- Dummy project templates
- Install hook analyzer
Phase 4: Integration
- API endpoint for triggering analysis
- Result aggregation with static analysis
- Dashboard integration
Cost Estimation
| Component | Specs | Cost/Analysis | Notes |
|---|---|---|---|
| Lambda | 3GB, 5min avg | ~$0.005 | Only runs when needed |
| S3 | 1MB result | ~$0.00002 | Minimal storage |
| VPC Endpoint | Per hour | ~$0.01/hr | Shared across analyses |
| Total | ~$0.015/analysis |
At 10,000 packages/day: ~$150/day = ~$4,500/month
Coordinator Notes
Owner: Agent 1 (Detection Engine) for sandbox execution logic Dependencies:
- Agent 2 (npm): Package tarball download
- Agent 3 (GraphQL): Trigger endpoint, result storage
- Agent 4 (CLI): Local sandbox mode for testing
Files to Create:
src/sandbox/
├── lambda/
│ ├── Dockerfile
│ ├── handler.ts
│ └── nsjail.cfg
├── monitors/
│ ├── network-monitor.ts
│ ├── fs-monitor.ts
│ ├── process-monitor.ts
│ ├── env-monitor.ts
│ └── resource-monitor.ts
├── execution/
│ ├── test-runner.ts
│ ├── dummy-projects.ts
│ └── install-monitor.ts
├── types/
│ └── sandbox-report.ts
└── local/
└── docker-sandbox.ts # For local testing
Terraform:
infra/terraform/
├── sandbox-lambda.tf
├── sandbox-vpc.tf
├── sandbox-s3.tf
└── sandbox-ecr.tf