DocsSandbox

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:

  1. Exfiltrate data to the internet
  2. Access or contaminate our detection tools
  3. Persist beyond the analysis session
  4. 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:

  1. 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
  2. File System Escape

    • Read-only root filesystem
    • Only /tmp writable (100MB tmpfs, noexec)
    • Mount namespace isolation
    • No access to host filesystem
  3. Process Escape

    • PID namespace isolation
    • No ptrace allowed
    • seccomp blocks dangerous syscalls
    • cgroup process limits
  4. 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
  5. Cross-Analysis Contamination

    • Lambda creates fresh environment each invocation
    • No persistent storage
    • No shared state between analyses
  6. 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):

  1. Package installation (npm install)
  2. Test suite execution (npm test)
  3. Basic code execution (require/import)
  4. File operations within /tmp
  5. Environment variable reads
  6. 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

ComponentSpecsCost/AnalysisNotes
Lambda3GB, 5min avg~$0.005Only runs when needed
S31MB result~$0.00002Minimal storage
VPC EndpointPer hour~$0.01/hrShared 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