The problem
Supply chain attacks keep increasing. Typosquatting, malicious install scripts, dependency confusion — the attack surface is the package manager itself. You can't really know if a package is safe without installing it. But installing untrusted code on your machine, or in a shared CI runner, is exactly the thing you're trying to avoid.
Existing tools either scan metadata without installing (incomplete) or run in shared environments (risky). I wanted something that does both: actually installs the package and runs real security tools against it — but in a throwaway environment that's destroyed immediately after.
Architecture
The whole thing is serverless. A browser or CLI hits a Lambda Function URL, the orchestrator Lambda spawns a MicroVM, the scan runs inside that VM, results go to Bedrock for analysis, and the VM is torn down.
- pip / npm install
- bandit · pip-audit
- ruff · npm audit
- OWASP dependency-check
- destroyed after scan
- CVSS severity scoring
- OWASP Top 10 mapping
- supply-chain risk flags
- structured JSON output
The flow, step by step:
- User submits a scan target (package name or repo URL + type) via the web UI or CLI
- The orchestrator Lambda receives it through a Function URL
- It spawns a Lambda MicroVM from a pre-built image
- The MicroVM installs the package and runs the security tools in sequence
- Raw results go to Bedrock Kimi K2.5 for analysis
- The MicroVM is terminated — zero residual state
- A structured JSON report is returned to the user
AWS services used
| Service | Role | Why this choice |
|---|---|---|
| Lambda MicroVMs | Isolated scan execution | Firecracker VM per scan, full filesystem, sub-second boot |
| Lambda | Orchestrator | Stateless, cheap, scales to zero |
| Function URL | API endpoint | No API Gateway overhead, no 30s timeout |
| Bedrock | AI analysis | Kimi K2.5 — ~$0.003/review, 256K context |
| S3 | Frontend + reports | Static hosting, versioned report storage |
| CloudFront | CDN | Cache frontend, global edge delivery |
| Lambda Layer | Dependencies | requests + boto3 with MicroVMs API support |
Why Lambda MicroVMs
The obvious question: why not just run these scans in a regular Lambda function or an ECS container?
| Approach | Isolation | Boot | Filesystem | Cost |
|---|---|---|---|---|
| Regular Lambda | Shared runtime | ~100ms | None | Cheapest |
| ECS Fargate | Container-level | 30–60s | Full | Moderate |
| Lambda MicroVMs | VM-level (Firecracker) | ~1s | Full | Per second |
MicroVMs hit the sweet spot: hardware-level isolation (a real separate kernel), sub-second boot from snapshot, a full filesystem to actually install into, and automatic destruction on termination. You get the security of a VM with the operational model of serverless.
lambda-microvms service yet. You have to ship a newer boto3 in a Lambda Layer for the MicroVMs API to work. This cost me hours of debugging.The MicroVM image
Everything starts from a pre-built MicroVM image — the base environment with the scan tools baked in. You spawn VMs from it on demand.
A running MicroVM
When a scan starts, a VM spins up with its own HTTPS endpoint and network namespace. Note the operational settings — max lifetime of 28,800 seconds (8 hours), but I terminate after 60 seconds of idle, and the scan itself takes ~1–2 minutes.
Lifecycle
RunMicroVM → PENDING → RUNNING → [run scan] → TerminateMicroVM → TERMINATED
Each VM:
• has its own network namespace
• gets a unique HTTPS endpoint (mvm-xxx.lambda-microvm.us-east-1.on.aws)
• requires an auth token (X-aws-proxy-auth header)
• can run up to 8 hours (I terminate after ~1–2 minutes)
• is completely destroyed — no snapshot, no residual storage
During testing I spawned and tore down 15 of them — every one isolated, every one destroyed after its scan.
The scanning pipeline
Each ecosystem has its own steps, but the pattern is the same: install → analyze → collect results.
PyPI
1. pip install {package}=={version} # actually installs in the VM
2. pip-audit # known CVEs
3. bandit -r /path/to/package/ # static security analysis
4. ruff check /path/to/package/ # code quality
5. OWASP Top 10 pattern checks # injection, crypto, SSRF, etc.
npm
1. npm install {package}@{version} # installs with scripts
2. npm audit # known vulnerabilities
3. check suspicious install scripts # postinstall / preinstall hooks
4. analyze dependency tree depth # transitive risk
Maven
1. download artifact from Maven Central
2. OWASP dependency-check # NVD CVE database
3. analyze transitive dependency tree
4. license compatibility check # copyleft detection
The important part: all of this runs inside the MicroVM. If a package has a malicious setup.py that tries to exfiltrate data or open a reverse shell, it only affects its own VM. The orchestrator talks to the VM over HTTPS with an auth token. Even if the package compromises the VM's runtime, it can't escape the Firecracker boundary.
Here's the raw scan output collected inside the VM for a PyPI scan of requests — you can see the actual install logs, exit codes, and per-step timing.
AI-powered review with Kimi K2.5
Raw scan output is useful but noisy. pip-audit returns CVE IDs, bandit returns line numbers, OWASP checks return patterns. A human still has to synthesize all of it into a risk decision. That's the job the AI reviewer does.
Why Kimi K2.5
| Model | Cost / review | Context | Code analysis |
|---|---|---|---|
| Claude 3.5 Sonnet | ~$0.02 | 200K | Excellent |
| GPT-4o | ~$0.015 | 128K | Excellent |
| Kimi K2.5 | ~$0.003 | 256K | Very good |
Kimi K2.5 is 5–7× cheaper than the alternatives and has the largest context window. For code security analysis it's more than enough. The reviewer prompt is heavily structured with:
- CVSS v3.1 severity scoring criteria
- OWASP Top 10 mapping (A01–A10)
- Supply chain risk indicators (typosquatting, obfuscated code, suspicious hooks)
- Ecosystem-specific threat patterns (PyPI vs npm vs Maven)
- A strict JSON output schema with confidence levels
not_assessed rather than assuming it passed. If exit code is non-zero but output is empty, flag it as scan_error. This eliminates false negatives where a crashed tool looks like a clean result.What the report looks like
The AI turns all that raw output into a risk verdict, a quality grade, and concrete recommendations — each mapped to an OWASP category where relevant. Here's a scan of one of my healthcare Lambda repos: LOW risk, quality B, with five specific recommendations.
The underlying JSON keeps every finding structured — CVE IDs, severity, affected component, the fix, and the OWASP category.
It works the same for any repo. Here's a scan of a different project that surfaced one HIGH-severity finding and mapped recommendations to OWASP A02, A03, and A09.
Frontend & UX
The frontend is a single HTML file — no build step, no framework — deployed to S3 behind CloudFront. It calls the Lambda Function URL directly (no API Gateway proxy); CORS is handled by the Function URL config with AUTH_TYPE=NONE.
- Dark / light theme toggle — persisted in localStorage
- Scan history — last 50 scans stored locally, re-viewable, downloadable
- Download reports — JSON (raw) or Markdown (human-readable)
- Package type tabs — PyPI, npm, Maven, Git, with context-aware inputs
- Per-check toggles — security, OWASP, quality, license, typosquat
Dark mode, mid-scan:
And the same thing in light mode:
The dependency layer
The whole MicroVMs API only works because of a Lambda Layer carrying a newer boto3. Without it, boto3.client('lambda-microvms') doesn't exist.
CI/CD with GitHub OIDC
Deployment is fully automated via GitHub Actions. No long-lived AWS credentials — it uses OIDC federation for keyless auth.
# .github/workflows/deploy.yml (simplified)
on:
push:
branches: [main]
permissions:
id-token: write # required for OIDC
jobs:
deploy:
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT:role/Github-OIDC-Role
- run: |
# build layer, zip code, deploy
aws lambda update-function-code ...
The pipeline builds the layer (requests + latest boto3), packages the orchestrator, deploys to Lambda, and syncs the frontend to S3 with a CloudFront invalidation. Total deploy time: ~45 seconds.
Cost breakdown
| Component | Per scan | Notes |
|---|---|---|
| Lambda Orchestrator | ~$0.0001 | 512MB, ~5s execution |
| Lambda MicroVM | ~$0.005 | ~60–120s runtime |
| Bedrock Kimi K2.5 | ~$0.003 | ~2000 tokens output |
| S3 storage | ~$0.0001 | negligible per report |
| Total | ~$0.008 – $0.013 | under a penny |
At 1000 scans/month, total cost is roughly $10. The MicroVM is the most expensive piece because it runs for a minute or two, but per-second pricing means you never pay for idle time.
What I learned
Lambda MicroVMs are real — but new
The API exists (boto3.client('lambda-microvms')), but the runtime's bundled boto3 is too old to include it. You must ship a newer boto3 via a Lambda Layer. The CLI (aws lambda-microvms) works if you have the latest AWS CLI.
Function URLs over API Gateway
For a project like this, Function URLs are vastly simpler than API Gateway — no 30-second timeout, built-in CORS, no routing rules. The tradeoff: no request validation, no usage plans, no custom domain without CloudFront.
CORS is always harder than you think
I went through it all: double CORS headers (Lambda and Function URL both setting them), OPTIONS in AllowMethods triggering a validation error (max 6 chars per method), and the classic "works in curl, fails in the browser" loop. Final fix: let the Function URL own all CORS and remove the headers from the Lambda code entirely.
The prompt matters more than the model
Switching from a generic "analyze this" prompt to a structured one — CVSS criteria, OWASP mapping, explicit edge-case handling — improved output quality far more than switching to a more expensive model would have.