14.3 Dynamic Analysis and Behavioral Testing¶
Static analysis examines code without executing it—but malicious packages often hide their true behavior until runtime. The event-stream attack (2018) used code that appeared benign statically: it decoded and executed a payload only when specific conditions were met at runtime. The ua-parser-js compromise (2021) downloaded and executed malware during installation, behavior invisible to source code inspection alone. Dynamic analysis—observing what code actually does when it runs—catches what static analysis cannot.
This section covers runtime behavior analysis techniques for dependencies, from installation-time monitoring to production behavioral baselines.
Why Static Analysis Isn't Enough¶
Static analysis has fundamental limitations that attackers exploit.
Runtime-Only Behaviors:
| Technique | Why Static Analysis Misses It |
|---|---|
| Dynamic code loading | Payload fetched from network at runtime |
| Conditional execution | Malicious code triggers only under specific conditions |
| Obfuscation | Heavily encoded payloads resist static parsing |
| Multi-stage payloads | Initial code is benign; downloads malicious code |
| Environment-dependent | Behavior changes based on environment variables |
| Time-delayed execution | Malicious behavior activates after delay |
Real-World Example: event-stream
The event-stream attack demonstrates runtime-only behavior:
// Stage 1: Appeared in flatmap-stream dependency
// Decoded AES-encrypted payload only when:
// 1. Running in specific Copay wallet application
// 2. Certain environment conditions met
// 3. After specific date
// Static analysis saw only: decode, decompress, eval
// Dynamic analysis would see: Bitcoin wallet credential theft
Static analysis tools saw obfuscated code but couldn't determine intent without execution context.
The Dynamic Analysis Advantage:
Dynamic analysis observes actual behavior:
- What network connections does the code make?
- What files does it read or write?
- What environment variables does it access?
- What processes does it spawn?
- What data does it transmit?
Runtime monitoring catches malicious packages that evade static analysis. When malicious code is obfuscated (base64 encoded, eval'd), static scans may pass clean—but runtime monitoring detects the behavioral anomaly (unexpected network calls during installation) and flags suspicious activity immediately.
Install-Time Behavior Monitoring¶
Package installation is a critical moment—install scripts run with user privileges before any application code executes.
What Happens During Installation:
npm install package-name
│
▼
┌─────────────────────────┐
│ Download package │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Extract to node_modules│
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Run preinstall script │ ← Code execution opportunity
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Install dependencies │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Run postinstall script │ ← Code execution opportunity
└─────────────────────────┘
Install Script Risks:
Install scripts (preinstall, postinstall, install) can:
- Execute arbitrary code
- Access the file system
- Make network requests
- Read environment variables (including secrets)
- Spawn processes
Monitoring Installation:
Using strace (Linux):
# Trace all system calls during npm install
strace -f -e trace=network,file npm install suspicious-package 2>&1 | tee install.log
# Filter for interesting activity
grep -E "(connect|sendto|open|execve)" install.log
Using dtrace (macOS):
# Monitor network and file activity
sudo dtrace -n 'syscall::connect:entry /execname == "node"/ { printf("%s\n", copyinstr(arg0)); }' &
npm install suspicious-package
Using Socket's safe-npm:
Containerized Installation:
# Dockerfile for isolated package testing
FROM node:20-slim
# Disable network after npm is ready
RUN apt-get update && apt-get install -y iptables
# Copy package.json only
COPY package.json /app/
WORKDIR /app
# Install with network disabled (except npm registry)
RUN npm install
# Check for unexpected files
RUN find /app -newer /app/package.json -type f
Network Call Detection and Analysis¶
Malicious packages often exfiltrate data or download payloads via network calls.
What to Monitor:
| Activity | Concern |
|---|---|
| Unexpected destinations | Calls to unknown IPs/domains |
| Install-time networking | Packages shouldn't phone home during install |
| DNS lookups | Unusual domain resolutions |
| Data transmission | Outbound data during build or install |
| Protocol anomalies | Non-HTTP traffic, unusual ports |
Network Monitoring Approaches:
1. DNS Monitoring:
# Capture DNS queries during installation
sudo tcpdump -i any -w dns.pcap port 53 &
npm install package-name
kill %1
# Analyze captured DNS
tcpdump -r dns.pcap -n | grep -E "A\?|AAAA\?"
2. HTTP/HTTPS Inspection:
# Use mitmproxy to inspect HTTPS traffic
mitmproxy --mode transparent --showhost &
# Set proxy
export HTTP_PROXY=http://localhost:8080
export HTTPS_PROXY=http://localhost:8080
export NODE_EXTRA_CA_CERTS=~/.mitmproxy/mitmproxy-ca-cert.pem
npm install package-name
3. Network Policy Enforcement:
# Kubernetes NetworkPolicy for build pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: build-isolation
spec:
podSelector:
matchLabels:
type: build
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/8 # Internal only
ports:
- port: 443 # Registry access only
Expected vs. Unexpected Network Behavior:
| Expected | Unexpected |
|---|---|
| npm registry | Unknown CDNs |
| GitHub (for git deps) | Pastebin, file sharing |
| Known CDNs (unpkg, jsDelivr) | Raw IP addresses |
| Company internal | ngrok, localtunnel |
File System Access Monitoring¶
Monitor what files packages read and write during installation and execution.
Concerning File System Activity:
| Activity | Risk |
|---|---|
Reading ~/.ssh/* |
Credential theft |
Reading ~/.aws/* |
Cloud credential theft |
| Writing to system paths | Persistence, privilege escalation |
Accessing .env files |
Secret exfiltration |
| Reading browser storage | Cookie/token theft |
Monitoring Techniques:
Using fswatch/inotify:
# Monitor file system changes during install
inotifywait -m -r --format '%w%f %e' \
-e access -e modify -e create \
/home /etc /tmp &
npm install package-name
# Review accessed files
kill %1
Using Linux capabilities restriction:
# Run npm with restricted capabilities
unshare --map-root-user --mount \
sh -c "mount -t tmpfs none /home && npm install package-name"
Audit file access with auditd:
# Add audit rules
sudo auditctl -w /home/user/.ssh -p r -k ssh-access
sudo auditctl -w /home/user/.aws -p r -k aws-access
# Run install
npm install package-name
# Check audit log
sudo ausearch -k ssh-access -k aws-access
Socket.dev and Similar Tools¶
Socket (socket.dev) provides automated behavioral analysis for npm and Python packages.
How Socket Works:
- Analyzes packages for suspicious behaviors
- Monitors install scripts and runtime behavior
- Detects supply chain attack indicators
- Integrates with GitHub PRs and CI/CD
Socket's Detection Categories:
| Category | Examples |
|---|---|
| Install scripts | Preinstall, postinstall execution |
| Network access | Fetches from unexpected URLs |
| Filesystem access | Reads sensitive files |
| Shell execution | Spawns child processes |
| Obfuscation | Encoded or minified code |
| Typosquatting | Name similar to popular package |
Integration Example:
# GitHub Action with Socket
name: Socket Security
on: [pull_request]
jobs:
socket:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: socketsecurity/socket-github-action@v1
with:
api_key: ${{ secrets.SOCKET_API_KEY }}
Similar Tools:
| Tool | Focus |
|---|---|
| Socket | npm, Python behavioral analysis |
| Snyk | SCA with some behavioral signals |
| Phylum | Package risk analysis, behavioral |
| Deps.dev | Package metadata and risk signals |
| npm audit signatures | Package integrity verification |
Sandboxing Approaches¶
Isolate package testing to contain potential damage.
Docker-Based Sandboxing:
# Minimal sandbox for package testing
FROM node:20-alpine
# Create non-root user
RUN adduser -D testuser
USER testuser
WORKDIR /home/testuser
# Install package in isolation
COPY package.json .
RUN npm install
# No network, read-only root
# docker run --network none --read-only ...
# Run with maximum isolation
docker run \
--network none \
--read-only \
--tmpfs /tmp \
--cap-drop ALL \
--security-opt no-new-privileges \
sandbox-image npm test
gVisor (Lightweight VM):
gVisor provides stronger isolation than containers:
# Run with gVisor runtime
docker run --runtime=runsc \
--network none \
sandbox-image npm install suspicious-package
gVisor intercepts system calls, providing: - Kernel-level isolation - System call filtering - Better security than standard containers
Firecracker (microVMs):
For maximum isolation, use microVMs:
# Firecracker provides hardware-level isolation
# Each package test runs in its own microVM
# Complete isolation from host and other tests
firecracker --config-file vm-config.json
Isolation Comparison:
| Approach | Isolation Level | Overhead | Complexity |
|---|---|---|---|
| Container (Docker) | Process | Low | Low |
| gVisor | System call | Medium | Medium |
| Firecracker | Hardware | Medium | High |
| Full VM | Hardware | High | High |
Production Runtime Monitoring¶
Beyond installation, monitor dependency behavior in production.
Runtime Monitoring Approaches:
1. System Call Monitoring:
# Use seccomp to restrict and log syscalls
# seccomp-bpf profile
{
"defaultAction": "SCMP_ACT_LOG",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["execve", "connect", "sendto"],
"action": "SCMP_ACT_LOG"
}
]
}
2. eBPF-Based Monitoring:
# Using bcc/eBPF to monitor network connections
from bcc import BPF
bpf = BPF(text="""
int trace_connect(struct pt_regs *ctx) {
bpf_trace_printk("connect called\\n");
return 0;
}
""")
bpf.attach_kprobe(event="tcp_v4_connect", fn_name="trace_connect")
3. Application-Level Monitoring:
// Node.js: Intercept require() and network
const originalRequire = Module.prototype.require;
Module.prototype.require = function(id) {
console.log(`Requiring: ${id}`);
return originalRequire.apply(this, arguments);
};
const originalFetch = global.fetch;
global.fetch = function(url, options) {
console.log(`Fetch to: ${url}`);
return originalFetch.apply(this, arguments);
};
4. Runtime Application Self-Protection (RASP):
Tools like Sqreen, Contrast Security, or open-source alternatives monitor application behavior:
- Function call interception
- Data flow tracking
- Anomaly detection
- Attack blocking
Behavioral Baselines and Anomaly Detection¶
Establish normal behavior baselines to detect anomalies.
Creating Baselines:
# Baseline: express@4.18.2
### Network Behavior
- Expected: None during require()
- Expected: Listens on configured port
- Unexpected: Outbound connections
### File System
- Expected: Reads package files
- Expected: Writes to configured log paths
- Unexpected: Access to ~/.ssh, ~/.aws, /etc/passwd
### Process
- Expected: Single Node.js process
- Unexpected: Child process spawning
Automated Baseline Generation:
#!/bin/bash
# Generate behavioral baseline
PACKAGE=$1
# Start monitoring
tcpdump -w network.pcap &
inotifywait -m -r --format '%w%f %e' /home > fs.log &
# Run package tests
npm test
# Stop monitoring
kill %1 %2
# Generate baseline report
echo "Network connections:"
tcpdump -r network.pcap -n | grep -v "localhost"
echo "File system access:"
cat fs.log | grep -v node_modules
Anomaly Detection:
| Baseline | Anomaly | Alert Level |
|---|---|---|
| No network during install | Network call detected | High |
| Reads only package files | Reads ~/.aws/credentials | Critical |
| No child processes | Spawns shell | Critical |
| Consistent CPU usage | Cryptocurrency mining patterns | High |
Continuous Monitoring Architecture:
┌─────────────────────────────────────────────────────────────┐
│ Production Application │
├──────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ App │ │ eBPF │ │ Network │ │ File System │ │
│ │ Code │ │ Probes │ │ Monitor │ │ Monitor │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │
└───────┼─────────────┼─────────────┼────────────────┼─────────┘
│ │ │ │
└─────────────┴──────┬──────┴────────────────┘
│
┌────────▼────────┐
│ SIEM / SOC │
│ Alert Engine │
└────────┬────────┘
│
┌────────▼────────┐
│ Response │
│ (Alert/Block) │
└─────────────────┘
Recommendations¶
For Security Practitioners:
-
Monitor installation. Treat
npm installas code execution—because it is. Run installations in monitored, sandboxed environments. -
Establish behavioral baselines. Document expected behavior for critical dependencies. Alert on deviations.
-
Use purpose-built tools. Socket.dev and similar tools automate behavioral analysis. Manual monitoring doesn't scale.
For DevOps Engineers:
-
Sandbox build environments. Build pipelines should use network-restricted containers. Legitimate builds rarely need arbitrary network access.
-
Implement egress controls. Allowlist expected destinations for build systems. Block unknown outbound connections.
-
Enable auditing. System call and network auditing in production enables investigation when anomalies occur.
For Organizations:
-
Layer your defenses. Static analysis catches some threats; dynamic analysis catches others. Use both.
-
Invest in visibility. You can't detect behavioral anomalies you're not monitoring. Implement runtime observability.
-
Assume compromise possibility. Design systems assuming a dependency might be malicious. Limit blast radius through isolation and least privilege.
Dynamic analysis reveals what code actually does rather than what it claims to do. In an ecosystem where installation scripts run with developer privileges and packages can execute arbitrary code, behavioral monitoring is essential. The goal isn't to analyze every package exhaustively—that's impractical—but to have visibility into critical moments (installation, initialization) and continuous baseline monitoring in production.