A local-first security scanner: run SAST, SCA, IaC, DAST, secret, container-image and supply-chain scans from one CLI — or a native desktop app — normalize every tool's output into one schema, and explore the results in a React dashboard.
Everything runs on your machine via Docker — no SaaS, no CI minutes required. A reference GitHub Actions workflow is included to show how the same scans fit into a pipeline when you want them there.
Security scans take effort, so they often just don't get run:
- The tools are fragmented. Semgrep, Trivy, Checkov, OSV-Scanner and ZAP each have their own CLI, flags and JSON shape, and just reading five different report formats is a chore.
- The dashboards that unify them want your code. Most tools that do aggregate results are SaaS: you upload your source, pay per seat, and burn CI minutes to get a view.
- Results end up scattered. Even after you run everything, the findings sit in five terminal dumps with no shared severity scale, no history, and no single place to triage them.
The goal here is to make a full local security pass one command (or one click) away. Every scanner sits behind one interface, every result is normalized into one schema, and one dashboard shows them all — on your machine, with your source and results never leaving it. Point it at a folder, a URL, or a container image, and read the results in one place. The CLI fits into CI; the desktop app runs the same scans without a terminal.
| Results (summary + findings) | Scan (folder / URL / image) |
|---|---|
![]() |
![]() |
| History (trend + past runs) | Scan Guide |
![]() |
![]() |
| Finding detail | Japanese UI (i18n) |
![]() |
![]() |
| Settings (Socket.dev key) | Scan in progress (toggles + cancel) |
![]() |
![]() |
| Category | Full name | What it checks | Tool |
|---|---|---|---|
| SAST | Static Application Security Testing | Source code, statically, for vulnerable patterns | Semgrep |
| SCA | Software Composition Analysis | Dependencies for known CVEs | Trivy fs |
| IaC | Infrastructure as Code | Misconfigurations in infra definitions | Checkov |
| DAST | Dynamic Application Security Testing | A running app, by attacking it | OWASP ZAP |
| Secret | Secret scanning | Hardcoded credentials, tokens and keys | Trivy |
| Image | Container image scanning | OS and package vulnerabilities in a built image | Trivy image |
| Supply chain | Supply-chain analysis | Dependencies vs the OSV vulnerability database | OSV-Scanner |
| Supply risk | Supply-chain risk | Install scripts & unpinned deps (local); behavioral analysis with a key | Socket.dev / local |
A filesystem scan (make scan) runs SAST + SCA + IaC + secret + supply-chain +
supply-risk together; DAST (--url) and container-image (--image) scans are
run on demand. Each scanner's native JSON is normalized into a single Finding shape
and aggregated into one scan-report.json that the dashboard reads.
- Everything except DAST is static. SAST, SCA, IaC, secret, container-image and supply-chain scans read your source, dependencies, infra config or image without running anything — no traffic is sent to the target, the app does not need to be running, and they never execute the scanned code, so there are no side effects on it.
- DAST is dynamic. It drives a running app and sends real requests. This finds runtime issues but can have side effects on the target, so only scan systems you own or are authorized to test.
This project runs the ZAP baseline (passive) scan: it spiders the site and inspects the responses (missing security headers, cookie flags, info disclosure). Impact is low, so it is reasonable even against production.
A full / active scan (not run here by default) injects attack payloads
(SQLi, XSS, …) to confirm exploitable issues. It is far more invasive — it can
create or delete data, send mail, or load the target — so reserve it for
staging or systems you own, and prefer the authenticated header
(--auth-header) only against test environments.
pnpm workspace
├── packages/core # Zod schema + types for the unified report (shared)
├── packages/cli # Orchestrates the Docker-based scanners, writes the report
└── packages/web # React + Vite dashboard that visualizes the report
See docs/architecture.md for the data flow, the
swappable parts (seams), and the supported running modes (local CLI, web viewer,
desktop app, and the planned hosted mode).
- Node.js ≥ 20 and pnpm ≥ 9
- Docker (Desktop or Engine) running locally — the scanners run as containers
On Docker Desktop for Mac/Windows,
--network=hostis unavailable. To scan a locally running app with DAST, targethttp://host.docker.internal:<port>instead oflocalhost.
pnpm install # install workspace dependencies
cp .env.example .env # adjust scan target / output path if needed
make scan # run the static suite (SAST + SCA + IaC + secret + supply)
make dev # open the dashboard (http://localhost:5174)make scan writes the unified report to the path in SCAN_REPORT_PATH
(default packages/web/public/scan-report.json), which the dashboard loads on
startup. A sample report ships in the repo so the dashboard has data on first
run.
Run scanners individually or all together:
make scan # SAST + SCA + IaC + secret + supply-chain (the static suite)
make scan-sast # Semgrep only
make scan-sca # Trivy only (dependency CVEs + secrets)
make scan-iac # Checkov only (skipped automatically when no *.tf files exist)
make scan-supply # OSV-Scanner only (supply-chain)
make scan-socket # supply-chain risk (local; Socket.dev with SOCKET_API_KEY)
make scan-dast # OWASP ZAP baseline — requires a target URL
make scan-image IMAGE=nginx:1.25 # Trivy container-image scanmake scan runs the full static suite; scan-dast (needs a URL) and
scan-image (needs an image) are opt-in.
DAST is opt-in because it needs a running target:
DAST_TARGET_URL=http://host.docker.internal:3000 make scan-dastDAST also supports multiple comma-separated targets and scanning behind auth (the header is injected into every request via ZAP's replacer):
pnpm --filter @devsec/cli scan -- --only dast \
--url http://host.docker.internal:3000,http://host.docker.internal:8080 \
--auth-header "Bearer eyJhbGci..."Want to see it surface real findings? A deliberately insecure target ships in
examples/vulnerable-app:
make scan SCAN_TARGET_DIR=examples/vulnerable-appDevelopment & quality:
make dev # Vite dev server for the dashboard
make build # build all packages
make lint # ESLint + Prettier check
make test # Vitest unit/integration suite (no Docker needed)
make integration # Real-scanner tests via Docker (Semgrep/Trivy/Checkov)
make e2e # Playwright browser tests
make typecheck # tsc --noEmit across packages
make help # list all targetsThe CLI also accepts flags directly:
pnpm --filter @devsec/cli scan -- \
--target ./my-project \
--only sast,sca \
--out ./scan-results/report.json \
--url http://host.docker.internal:3000
# scan a container image
pnpm --filter @devsec/cli scan -- --only image --image nginx:1.25Selectable --only types: sast, sca, iac, dast, image, supply, socket
(secrets are emitted by the sca scan). The socket scan adds Socket.dev
behavioral analysis when SOCKET_API_KEY is set (in the desktop app, via
Settings); otherwise it runs local heuristics only.
A quick loop to see the whole thing working end to end:
-
Scan the bundled vulnerable target and open the dashboard:
make scan SCAN_TARGET_DIR=examples/vulnerable-app make dev
The dev server uses port
5174by default and falls back to the next free port (e.g.5175) when it is taken — check the Vite output for the actual URL. -
Explore the Results view (summary + findings table), open a finding for details, and read the Scan Guide.
-
Build a trend: re-scan a few times (fix something, or point at another project). Every run is appended to the History tab, so you can watch the severity counts move over time.
-
Try DAST safely against a target you own (baseline is passive):
DAST_TARGET_URL=http://host.docker.internal:3000 make scan-dast
-
Run the quality gates:
make lint && make test # fast, no Docker make integration # real scanners (needs Docker) make e2e # browser tests
- Scan — enter a local folder (SAST/SCA/IaC/secret/supply-chain), a URL
(DAST), or a container image, and run a scan from the UI; you can also drag &
drop an existing
scan-report.jsonhere to inspect it. This needs a native runner, so scanning is active in the desktop app; in the browser it shows guidance and the upload box instead. - Results — one scan on a page: a visual summary (totals, severity donut, findings-by-type bar chart, per-scanner status) followed by a sortable, filterable table with a detail drawer (file/line, CVE/CWE, package, remediation). Clicking a summary card or scanner row narrows the table below. The browser opens here; the desktop app reaches it by drilling in from a scan or a history run.
- History — trend of findings by severity across past runs, plus a table of
every scan (click to reopen it). The CLI appends each
make scanto a local, file-based history store (history/index.json+ one file per run); the desktop app records the scans it runs inlocalStorage. No database, and the index shape mirrors a future hostedscan_runstable. - Scan Guide — explains every category (SAST, DAST, SCA, IaC, secret, container image, supply-chain, supply-risk) and generates copy-paste CLI commands for each (the DAST command updates as you type a target URL).
- Settings (desktop) — store a Socket.dev API key so the supply-risk scan adds behavioral analysis; it's kept locally and passed to the scan.
A report looks like this:
Two GitHub Actions workflows ship with the repo:
.github/workflows/ci.yml— quality gate (lint → type-check → test → build) on every push and PR..github/workflows/security-scan.yml— reference pipeline running the same scanners (Semgrep, Trivy, Checkov, OSV-Scanner, and ZAP on demand). It usesworkflow_dispatch(manual trigger) by default so it never consumes Actions minutes on its own — switch it topush/pull_requestto gate merges in a real project.
The scanners run as Docker containers, so the CLI needs a host with a Docker daemon:
- Local (default). Run
make scanon your machine — your source and the findings never leave it. The outbound traffic is pulling scanner images and their vulnerability databases, plus dependency lookups against public vulnerability APIs: OSV-Scanner queries OSV.dev with package names/versions by default (an offline DB mode is available), and the Socket.dev supply-risk check is opt-in (off unless you set a key). Neither sends your source code. - A VM with Docker (e.g. a small cloud VM / Fly.io machine). The CLI works unchanged because it can still launch sibling containers.
This is a focused demo. In scope: the CLI, the unified report, the dashboard, and the reference CI workflow. Out of scope: running scans from the web UI, authentication, SIEM/alerting integrations, and production monitoring.








{ "schemaVersion": "1", "generatedAt": "2026-06-30T09:15:00.000Z", "target": "demo-app", "scanners": [ { "scanner": "semgrep", "type": "sast", "status": "ok", // ok | skipped | error "durationMs": 8421, "findings": [ { "id": "semgrep:...:src/db/query.ts:42", "scanner": "semgrep", "type": "sast", "ruleId": "...tainted-sql-string", "title": "tainted-sql-string", "severity": "high", // critical | high | medium | low | info | unknown "message": "...", "file": "src/db/query.ts", "startLine": 42, "cwe": ["CWE-89: SQL Injection"], "remediation": "Use parameterized queries.", }, ], }, ], "summary": { "total": 13, "bySeverity": { "...": 0 }, "byType": {}, "byScanner": {}, }, }