Skip to content
aliamanataliPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

secintel

Zero-config source-code security auditor for JavaScript / TypeScript and Python.

npm version CI license MIT provenance

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.

Quick start

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 clean

Step 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.

What a finding looks like

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.1s

More:

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 step

Adopting on a legacy codebase

Baseline 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 finding

Baselines are fingerprinted by rule + file + code content, so they survive line shifts from unrelated edits.

Suppressing a finding

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.

Configuration (optional)

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.

What it finds

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).

Coverage at a glance

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.

What each category catches

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/…, FastAPI request.query_params and Query()/Form()/Body()/Path() parameters, Django request.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.

Architecture: a language-neutral core plus analyzers

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.

CI

GitHub Actions (annotations in the Security tab)

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.

GitLab CI

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.

Any other CI

npx secintel --fail-on high      # exit 1 on high/critical findings

Using secintel as a library

The 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.

Performance

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.

Security posture of secintel itself

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.

Non-goals (v1)

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.

License

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages