Zero-config source-code security auditor for JavaScript / TypeScript and Python.
npm audit tells you whether your dependencies have known CVEs. secintel answers the more important question: is the code you actually wrote insecure? Hardcoded API keys, SQL built by string concatenation, eval() on request input, permissive CORS, missing cookie flags — found locally, explained in plain language, fixed with copy-pasteable guidance.
- Zero-config.
npx secintel— no rules DSL, no account, no cloud. - First-party code only.
node_modules, build output, virtualenvs, and minified files are never scanned. - Whole-project by default. Point it at a monorepo root and it scans every stack: JS/TS/JSX/TSX and Python both get full AST analysis with taint tracking, and secret detection covers all text files —
.env,.yml, Dockerfiles, whatever is there. No flags, no project-type detection. - Low noise over high recall. AST-based rules with intra-function taint tracking; every rule has a confidence tier and low-confidence heuristics are off by default. Secret rules skip test/fixture paths automatically.
- CI-native. Deterministic exit codes, JSON + SARIF 2.1.0 output, and a baseline mode so legacy projects can adopt incrementally.
- Local and private. No telemetry. No code leaves your machine.
Three lines from zero to a CI gate:
npx secintel # 1. see what's wrong, right now
npx secintel --baseline # 2. accept today's findings (optional — for legacy repos)
npx secintel init ci # 3. scaffold the CI workflow that keeps it cleanStep 1 is the whole tool: exit code 0 means clean (or nothing at/above the fail threshold), 1 means findings, 2 means tool error. Step 2 is only for codebases with a backlog you can't fix today. Step 3 detects your CI platform, writes the workflow, and never touches an existing one.
Real output from npx secintel on this repo's monorepo fixture — one Python file, one TypeScript file, and a .env, scanned in a single pass with no configuration. Trimmed for length: the terminal shows a ±2-line code frame and the full fix guidance for every finding. (The credential's own code frame is dropped here on purpose — secintel's release gate scans its own tarball, and a real key pattern in this README would fail it.)
$ npx secintel
━━ backend/ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12 findings
── CRITICAL ────────────────────────────────────────────────
CRITICAL [SI-SEC-001] AWS access key ID detected: AKIAIO…(20 chars)
backend/.env:3:19
CWE-798 · OWASP A07:2021 · secrets · confidence: high
Fix: Revoke the credential immediately (assume it is compromised), then load it …
CRITICAL [SI-PY-INJ-001] SQL passed to execute() is built from request input — SQL injection
backend/app/main.py:37:20
> 37 │ cursor.execute(f"SELECT id, email FROM users WHERE id = '{user_id}'")
CWE-89 · OWASP A03:2021 · injection · confidence: high
Fix: Pass parameters separately and let the driver bind them: `cursor.execute( …
CRITICAL [SI-JS-INJ-002] User-controlled input reaches exec() — command injection
frontend/lib/thumbnails.ts:5:3
> 5 │ exec("convert " + req.query.file + " -resize 100x100 thumb.png");
CWE-78 · OWASP A03:2021 · injection · confidence: high
Fix: Use execFile/spawn with an argument array: `execFile('ls', ['-la', dir])`. …
16 findings (6 critical, 9 high, 1 medium) · 7 files · 5 analyzed, 2 skipped (no analyzer) · 0.1sMore:
npx secintel ./src ./api # scan specific paths
npx secintel --format json # machine-readable output
npx secintel --format sarif -o scan.sarif # GitHub code scanning
npx secintel --format html -o report.html # client-ready audit report
npx secintel --fail-on medium # fail CI on medium+ (default: high)
npx secintel --rules # list all rules
npx secintel init # generate secintel.config.js
npx secintel init ci --with-baseline # baseline + CI workflow in one stepBaseline the findings you already have; from then on CI fails only on new findings:
npx secintel --baseline # snapshot to .secintel-baseline.json (commit it)
npx secintel # exit 0 — existing findings are baselined
# …someone adds new SQL concatenation…
npx secintel # exit 1, reporting only the new findingBaselines are fingerprinted by rule + file + code content, so they survive line shifts from unrelated edits.
A reason is required — reasonless ignores are themselves reported as info:
// secintel-ignore SI-JS-INJ-001 -- reviewed, input is enum-validated upstream
db.query(`SELECT * FROM reports_${quarter}`);secintel-ignore-file SI-XXX-000 -- reason at the top of a file suppresses for the whole file.
npx secintel init writes a commented secintel.config.js:
export default {
include: ["src", "api"],
exclude: ["**/*.test.ts", "fixtures/**"],
failOn: "high", // critical | high | medium | low
rules: {
"SI-JS-CFG-004": "off",
"SI-JS-CRY-001": { severity: "high" },
},
enableLowConfidence: false,
hints: true, // set false to silence terminal tips
};When a scan finds something in a git repo that has no secintel workflow, the summary ends with a one-line tip pointing at npx secintel init ci. It is advice only — it never affects the exit code, and hints: false turns it off.
66 rules across six vulnerability categories, plus one meta rule for suppression hygiene — see RULES.md for the full auto-generated reference.
Rule ids name the analyzer that owns them: SI-JS-* for JavaScript/TypeScript, SI-PY-* for Python, and SI-SEC-* / SI-META-* for the language-independent core (secret scanning runs on every text file, in any language).
Rule counts per category, by what the rules can see. "All files" rules are language-independent — they run on .env, YAML, JSON, Dockerfiles, Go, Rust, anything text.
| Category | JS / TS (AST) | Python (AST) | All files |
|---|---|---|---|
| Secrets | 3 | 3 | 4 |
| Injection | 10 | 7 | — |
| Misconfiguration | 8 | 7 | — |
| Crypto | 7 | 5 | — |
| XSS | 5 | 3 | — |
| Unsafe patterns | 3 | — | — |
| Meta (suppression hygiene) | — | — | 1 |
| Total | 36 | 25 | 5 |
Python has no unsafe-patterns rules because prototype pollution has no Python analogue; the ReDoS and body-size rules have not been ported yet. Everything else is at parity.
| Category | JavaScript / TypeScript | Python |
|---|---|---|
| Secrets | Entropy analysis on secret-named variables, hardcoded passwords, JWT signing secrets | Same three, plus literal password= kwargs on DB drivers |
| Injection | SQL/NoSQL via concatenation or $where, command injection (exec, shell: true), path traversal into fs, eval/new Function/vm, string timers, tainted require() |
SQL via f-string/%/.format() into cursor.execute/ORM raw, os.system/shell=True, path traversal into open/shutil, eval/exec, pickle/yaml.load, Jinja template injection |
| Misconfiguration | CORS wildcard + credentials, cookie flags, hardcoded session secrets, rejectUnauthorized: false, stack traces in responses, express.static on the project root |
CORSMiddleware/flask-cors wildcard + credentials, DEBUG = True, hardcoded SECRET_KEY, verify=False, cookie flags, dev server on 0.0.0.0, assert used for authorization |
| Crypto | MD5/SHA-1, passwords without bcrypt/argon2, Math.random() tokens, createCipher, static IVs, ECB mode |
MD5/SHA-1, passwords via hashlib, random for tokens, DES/RC4/ECB, static IVs |
| XSS | dangerouslySetInnerHTML, innerHTML, document.write, javascript: URI sinks, reflected input in res.send() |
mark_safe/Markup on request input, reflected input in HTMLResponse/HttpResponse, open redirects |
| Unsafe patterns | Prototype pollution (obj[userKey], tainted merges), ReDoS-prone regexes, unbounded body parsing |
— |
Secrets are language-independent and run on every text file regardless of analyzer: provider key patterns (AWS/Stripe/GitHub/Google/Slack/OpenAI/Anthropic/private key blocks), .env not gitignored, docker-compose and JSON config credentials, and credentials embedded in URLs — in .py, .go, Dockerfiles, YAML, anything.
Detection is AST-based (single parse per file, one visitor pass for all rules) with pragmatic intra-file, intra-function taint tracking. Sources are the ones each ecosystem actually uses:
- JS/TS —
req.body/query/params/headers/cookies,process.argv,location.*, WebSocket message handlers - Python — Flask
request.args/form/json/…, FastAPIrequest.query_paramsandQuery()/Form()/Body()/Path()parameters, Djangorequest.GET/POST/body/…,sys.argv,input()
Values passing through sanitizer-looking calls (escape*, sanitize*, path.basename, shlex.quote, bleach.clean, …) or a type coercion (int(), float()) drop their taint to keep noise low.
src/core/ language-neutral: file walking, routing, findings, suppression,
baseline, cache, reporters, and the core secrets scanner
src/analyzers/ pluggable language support (javascript, python)
src/cli/ a thin consumer of the core
The core never imports from src/analyzers/ — registration is the only coupling point, and adding Python touched exactly one existing file (src/scan.ts, to register it). An analyzer is one object:
import { createScanner } from "secintel";
const rubyAnalyzer = {
id: "ruby",
extensions: [".rb"],
commentStyles: [{ line: "#" }],
rules: [/* rule metadata, for --rules, SARIF, and the docs */],
analyze(files, ctx) {
return files.flatMap((file) => /* … */ [ctx.createFinding({ rule, file: file.path, line })]);
},
};
const result = await createScanner({ analyzers: [rubyAnalyzer] }).scan({ cwd });The core handles everything else for it: routing files by extension, snippets and code frames, fingerprints, secintel-ignore suppression parsed with that language's comment markers, baselining, and all four report formats. Analyzers may optionally declare a worker-thread entry point for parallel analysis.
Mixed repos. Every discovered text file is routed to whichever analyzer claims its extension; files no analyzer claims are still scanned for secrets and reported in the summary as 87 analyzed, 34 skipped (no analyzer). When a scan spans multiple analyzers and multiple top-level directories, the terminal and HTML reports group findings by directory (frontend/, backend/) — single-stack projects render exactly as before. --fail-on and baselines stay project-wide.
npx secintel init ci writes this to .github/workflows/secintel.yml for you:
name: secintel
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
security-events: write
jobs:
secintel:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npx secintel --format sarif -o results.sarif --fail-on high
- if: always() # upload even when the scan failed the job
uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: results.sarif }The scan step both writes the SARIF and sets the exit code, so one step gates the build and annotates the PR. --fail-on in the generated workflow follows your secintel.config.js.
npx secintel init ci --platform gitlab writes an includable job to .gitlab/secintel.gitlab-ci.yml — it never edits your .gitlab-ci.yml, it prints the one-line include: to add.
npx secintel --fail-on high # exit 1 on high/critical findingsThe engine is a pure library — no console output, no process.exit:
import { scan, computeExitCode, renderHtml } from "secintel";
const result = await scan({ cwd: "/path/to/project" });
for (const f of result.findings) {
console.log(`${f.severity} ${f.ruleId} ${f.file}:${f.line} — ${f.message}`);
}
process.exitCode = computeExitCode(result, "high");
// or: fs.writeFileSync("report.html", renderHtml(result));Also exported: createScanner() (register your own analyzers), analyzeSource() (scan a string), renderTerminal/Json/Sarif/Html, ALL_RULES, RULE_OWNERS, baseline helpers, and all types.
The JSON reporter is versioned: schemaVersion: 2 adds analyzerId to every finding plus filesAnalyzed/filesSkipped and the analyzer list to the summary.
JS/TS files are analyzed in a worker_threads pool sized to your CPUs, with a single parse shared by all rules. A 120-file project scans in ~0.2s; --cache enables content-hash incremental runs for large repos.
Python is analyzed in-process. Its WebAssembly parser costs ~30ms to initialize and is loaded lazily, so a project with no .py files never pays for it at all.
A security tool must itself be clean: no install scripts, no native builds, no network access, and 5 production dependencies — @typescript-eslint/typescript-estree, commander, fast-glob, picocolors, and web-tree-sitter. secintel scans its own codebase with zero findings in CI, and publish is intended to be provenance-signed (npm publish --provenance).
The Python grammar is a WebAssembly build of tree-sitter-python (MIT, Max Brunsfeld), committed under vendor/ and shipped in the package. The native tree-sitter-python binding would have been faster, but it runs node-gyp-build on install — and a security scanner that executes install scripts to analyze your code is a poor advertisement for itself. Nothing is fetched at install time.
Dependency CVE scanning (use npm audit / pip-audit), auto-fix, and dynamic analysis. Deep (AST) analysis covers JS/TS/JSX/TSX and Python; other languages get secret scanning until their analyzer lands. On the v2 list: cross-file taint, a custom rule API, Go and Ruby analyzers, Vue/Svelte SFCs, Jinja/Django template files, and --watch.
MIT — see LICENSE. Copyright (c) 2026 Ali Aamanat Ali.
You may use, modify, and redistribute this in commercial and closed-source products; the only condition is that the copyright and permission notice travel with substantial portions of the code.
Every production dependency is permissive (MIT, ISC, Apache-2.0, or BlueOak) with no copyleft anywhere in the tree. The one piece of third-party material redistributed inside the published package is the vendored tree-sitter-python grammar (MIT, Max Brunsfeld); its notice is reproduced in THIRD-PARTY-NOTICES.md and shipped at vendor/tree-sitter-python.LICENSE. Attribution for web-tree-sitter, which loads that grammar, ships at vendor/web-tree-sitter.LICENSE.