security-scan
v0.1.0
Published
Unified security scanner (SAST + dependency CVEs + secrets) that orchestrates open-source engines and emits one report.
Maintainers
Readme
security-scan
One command, one report: SAST (code) + dependency CVEs + secrets.
security-scan orchestrates best-in-class open-source engines and merges
their output into a single, filterable HTML report (plus SARIF + JSON). You own
the rules and the report; the heavy analysis engines are reused, not rebuilt.
| Category | Engine | Finds |
|----------|--------|-------|
| Code (SAST) | Semgrep + bundled rules | WebView injection, PII in logs, insecure randomness, unsafe storage, hardcoded secrets |
| Dependencies (SCA) | OSV-Scanner + npm audit | Known CVEs in npm deps — deduped across both |
| Secrets | gitleaks | Committed API keys / tokens |
It's designed for JavaScript / TypeScript / React Native projects.
Install
npm install --save-dev security-scanThen install whichever external scanners you want (any that are missing are skipped gracefully — the report tells you which ran):
brew install semgrep osv-scanner gitleaks # npm audit ships with npmQuick start
npx security-scan init # scaffold config + pre-push hook into your repo
npx security-scan # run a full scan → security-report/report.htmlThe report is interactive: filter by severity, and expand each finding for a "what it is & impact" explanation.
Usage
security-scan [options] run a scan (report-only by default)
security-scan init scaffold config + pre-push hook in this repo
--only=sast,sca,secret run only these categories
--changed scan only changed code files (fast; for pre-push)
--pre-push gate: block only on secrets or CRITICAL findings
--ci gate iff SECURITY_ENFORCE=true (CI mode)
--gate=<level> gate at/above severity (critical|high|medium|low)
--update-baseline accept all current findings as the baseline
--offline skip network steps (registry packs, npm audit)
--out=<dir> output directory (default: security-report)
--base=<ref> git base ref for --changedSuggested package.json scripts:
{
"scripts": {
"security:scan": "security-scan",
"security:baseline": "security-scan --update-baseline",
"security:ci": "security-scan --ci"
}
}Outputs
Written to security-report/ (git-ignore it):
report.html— interactive human report (severity filter + descriptions)results.sarif— SARIF 2.1.0 for CI / code-scanning toolingsummary.json— machine-readable counts + findings
Configuration
Create security-scan.config.json (or add a "securityScan" key to
package.json). All fields are optional — these are the defaults:
{
"scanners": { "sast": true, "sca": true, "secret": true },
"lockfile": "package-lock.json",
"outDir": "security-report",
"baselineFile": ".security/baseline.json",
"excludes": ["node_modules", "ios", "android", ".git", "dist", "build"],
"semgrep": {
"useBundledRules": true,
"rules": ["security/my-extra-rules.yml"],
"packs": ["p/typescript", "p/react", "p/javascript", "p/secrets"],
"timeout": 30
},
"gitleaks": { "config": null },
"gate": { "level": "high" }
}semgrep.rules— your own rule files, run in addition to the bundled ones. SetuseBundledRules: falseto run only yours.semgrep.packs— Semgrep registry rule packs (fetched over the network).gitleaks.config— path to your own gitleaks config; otherwise the bundled one (which excludesnode_modules, build dirs, etc.) is used.
The baseline
The first scan of a mature repo finds a backlog. To keep that from blocking
every build, security-scan supports a baseline — accepted findings that are
reported but never gate:
npx security-scan --update-baseline # writes .security/baseline.json (commit it)After that, only new findings can fail a gated run.
Gating in CI
--ci is report-only until you opt in:
- Triage, then
npx security-scan --update-baselineand commit the baseline. - Set env
SECURITY_ENFORCE=true. - Optionally set
SECURITY_GATE_LEVEL=critical|high|medium(defaulthigh).
Example (Bitbucket Pipelines):
pipelines:
pull-requests:
'**':
- step:
name: Security scan
script:
- npm ci
- npx security-scan --ci
# after-script runs even if the gate failed the step — good place to
# notify. (see "Notifications" below)
after-script:
- node scripts/notify-security.mjs || true
artifacts:
- security-report/**Notifications (chat / report hosting)
security-scan intentionally only writes files to security-report/ — it does
not post to Slack/Teams/Chat itself, so the package stays provider-agnostic.
Wire notifications in your own after-script by reading summary.json and
sending wherever you like. Two things to know:
- Put it in
after-script, notscript, so it also fires when the gate fails the build. - Most chat webhooks can't attach a file — host
report.htmlsomewhere (S3, Google Drive, a static bucket) and send a link in the message.
Minimal scripts/notify-security.mjs (posts counts + a link to a hosted report):
import fs from 'node:fs';
const webhook = process.env.CHAT_WEBHOOK;
const s = JSON.parse(fs.readFileSync('security-report/summary.json', 'utf8'));
const c = s.counts;
// upload security-report/report.html to your host of choice, then:
const reportUrl = process.env.REPORT_URL || '(see build artifacts)';
await fetch(webhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `🔒 Security scan — ${s.total} findings `
+ `(🔴 ${c.critical} 🟠 ${c.high} 🟡 ${c.medium}) · ${reportUrl}`,
}),
});🔒 The report enumerates vulnerabilities — host it with access control (members only), not a public "anyone with link" URL.
Pre-push hook
security-scan init installs a .githooks/pre-push that runs a fast,
changed-files-only scan and blocks a push only on a secret or CRITICAL issue
in your changes. Bypass with git push --no-verify, or skip with
SKIP_SECURITY_SCAN=1 git push.
How it works
Each scanner's output is normalized into one finding model; dependency CVEs from OSV-Scanner and npm audit are merged by shared advisory id (GHSA/CVE) so each vulnerability is listed once with all sources. A missing scanner degrades to "skipped" (never a hard failure), and a broken Semgrep rule config is surfaced as an error rather than a silent "0 findings".
Programmatic use
import { runScan } from 'security-scan';
const exitCode = await runScan({ ci: true }); // 0 = ok, 1 = gate failedLicense
MIT
