npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

axiom-scan

v3.9.0

Published

Find the invariants your codebase assumes but never tests

Readme

axiom-scan

Find the invariants your codebase assumes but never tests.

85% line coverage doesn't mean your code is safe. It means 85% of your lines ran during tests — not that the assumptions those lines make have ever been challenged.

Every non-trivial codebase is full of implicit invariants:

  • user.subscription is never null before getBillingPlan() is called
  • initDB() always runs before query()
  • mu.Lock() is always paired with a deferred mu.Unlock()
  • errors from network calls are always surfaced, never silently dropped
  • SQL queries are never built with string concatenation
  • file paths are never constructed from user input without validation

These assumptions are true by convention, not by contract. Nobody wrote them down. Nobody tests them. When they break — due to a refactor, a new code path, a missing guard, or a security gap that's been there since day one — production breaks and the test suite stays green.

AXIOM reads your code statically, infers what it assumes to be true, diffs that against your test suite, and hands you a ranked list of the bets you're making on code that's never been verified.


Install

npm install -g axiom-scan

Or run without installing:

npx axiom-scan scan ./src

Demo

$ axiom scan ./src

  AXIOM v3.9.0  —  static invariant analysis
  target: /your/project

  ▸ TypeScript/JS   142 source files
  ▸ Go               91 source files
  ▸ Python           38 source files

  271 source files  ·  running inference...

  invariants found:
    security       5
    null          47
    concurrency   12
    resource       9
    swallowing     8

  ━━━ UNTESTED INVARIANTS  (ranked by blast radius)  ━━━

  ● CRITICAL  src/api/files.go:34  [high confidence]
    `os.Open()` with a string-concatenated path — susceptible to path traversal if a dynamic segment contains `../` (CWE-22)
    ├─ relied on by 3 call sites
    ├─ nearest public entry: GET /download  (1 hop)
    ├─ tests covering null path: 0
    └─ fix → Use `filepath.Join` + `filepath.Clean` and verify the resolved path starts with the base directory
         joined := filepath.Join(baseDir, segment)
         if !strings.HasPrefix(filepath.Clean(joined), filepath.Clean(baseDir)) {
             return errors.New("path traversal attempt")
         }

  ● CRITICAL  src/api/payments.ts:88  [high confidence]
    user.address assumed non-null
    ├─ relied on by 14 call sites
    ├─ nearest public entry: POST /checkout  (2 hops)
    └─ tests covering null path: 0

  ◐ HIGH  src/workers/job.go:134  [high confidence]
    `mu.Lock()` is not deferred — `Unlock` will not run on early return or panic
    ├─ relied on by 3 call sites
    ├─ nearest caller: processJob
    └─ tests covering null path: 0

  ─────────────────────────────────────────────────────
  81 invariants  ·  scanned 312 files in 2.4s

  5 critical  ·  18 critical  ·  25 high  ·  38 medium
  security ×5  null ×47  concurrency ×12  resource ×9  swallowing ×8

  axiom explain <file:line>  →  full remediation card

What it detects

| Type | Languages | What it catches | |------|-----------|-----------------| | security | Java, Go, Python, Ruby | Hardcoded credentials, SQL injection, command injection, path traversal, weak crypto, insecure deserialization — with CWE references and fix snippets | | null | All 8 | Property access on values that could be null/nil/undefined with no guard | | ordering | All 8 | Fields or variables read before they are guaranteed to be initialized | | shape | All 8 | UUID/type format assumed at point of use without validation | | swallowing | All 8 | Errors caught and silently discarded — empty catch, log-only catch | | concurrency | Go, Java, Python, C#, Ruby, Rust | Mutex leaks, unjoined threads, fire-and-forget tasks | | resource | All 8 | File handles, DB connections, sockets opened without guaranteed close — including inter-procedural: wrapper functions that return handles are tracked across call sites |


Security invariants (v3)

Security findings are ranked first — a security invariant with zero call sites still scores HIGH minimum. Every finding includes a CWE reference and a ready-to-paste fix snippet.

Hardcoded credentials (CWE-798)

Flags password, api_key, secret, access_token, private_key and similar names assigned to string literals. Placeholder strings ("change-me", "your-api-key", "xxx") are suppressed.

// Flagged
String password = "hunter2";

// Fix
String password = System.getenv("DB_PASSWORD");

Supported in: Java, Go, Python, Ruby


SQL injection (CWE-89)

Flags query methods called with a string built via concatenation or interpolation instead of parameterized placeholders.

# Flagged
cursor.execute("SELECT * FROM users WHERE name = '" + name + "'")

# Fix
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))

Supported in: Java (createQuery, nativeQuery, prepareStatement, executeQuery, executeUpdate), Go (Query, QueryRow, Exec, Prepare, Raw, Where), Python (execute, executemany, executescript, raw), Ruby (where, having, find_by_sql, execute, order, select)


Command injection (CWE-78)

Flags shell execution where the command or its arguments are dynamically constructed.

// Flagged: shell -c with dynamic script
exec.Command("sh", "-c", userInput)

// Fix
exec.Command("/usr/bin/myexe", "--flag", sanitizedInput)
# Flagged: backtick with interpolation
`rm -rf #{path}`

# Fix
require 'open3'
out, status = Open3.capture2("/usr/bin/myexe", "--flag", sanitized_input)

Supported in: Java (Runtime.exec() with non-literal), Go (exec.Command with dynamic executable or shell -c pattern), Python (subprocess.run/call/Popen with shell=True + dynamic arg; os.system/os.popen with non-literal), Ruby (backtick with interpolation; system/exec/spawn with dynamic arg)


Path traversal (CWE-22)

Flags file I/O calls where the path is constructed from user-controlled input, allowing ../ sequences to escape the intended base directory.

// Flagged
File f = new File(baseDir, userFilename);
Path p = Paths.get("/uploads", userPath);

// Fix
Path resolved = Paths.get(baseDir).resolve(userFilename).normalize();
if (!resolved.startsWith(Paths.get(baseDir).normalize())) {
    throw new SecurityException("path traversal attempt");
}
# Flagged
with open(f"{base_dir}/{filename}") as f:
    return f.read()

# Fix
import os
resolved = os.path.realpath(os.path.join(base_dir, filename))
if not resolved.startswith(os.path.realpath(base_dir) + os.sep):
    raise ValueError("path traversal attempt")

Supported in: Java (new File(base, dynamic), Paths.get() with non-literal segment), Go (os.Open/os.Create/os.ReadFile/os.WriteFile with + concat path), Python (open() with f-string or concat path), Ruby (File.open/File.read/File.write with interpolated path; send_file with dynamic path)


Weak cryptographic algorithm (CWE-327)

Flags use of broken hash functions and ciphers: MD5, SHA-1, DES, RC4.

// Flagged
MessageDigest md = MessageDigest.getInstance("MD5");

// Fix
MessageDigest md = MessageDigest.getInstance("SHA-256");
// For passwords: use BCrypt

Supported in: Java (MessageDigest.getInstance("MD5"/"SHA-1")), Go (md5.New, sha1.New, md5.Sum, sha1.Sum, des.NewCipher, rc4.NewCipher)


Insecure deserialization (CWE-502)

Flags deserialization of arbitrary objects from untrusted data.

# Flagged
data = pickle.loads(raw_bytes)

# Fix
import json
data = json.loads(raw)  # safe for untrusted input
# Flagged
config = yaml.load(stream)

# Fix
config = yaml.safe_load(stream)
# or: yaml.load(stream, Loader=yaml.SafeLoader)

Supported in: Python (pickle.loads, pickle.load, yaml.load without SafeLoader)


axiom explain — full remediation cards

axiom explain <file:line> rescans the project, locates the invariant at that position, and renders a full remediation card:

$ axiom explain src/api/files.go:34

  ══════════════════════════════════════════════════════════════════
  CRITICAL  src/api/files.go:34

  `os.Open()` with a string-concatenated path — susceptible to path
  traversal if a dynamic segment contains `../` (CWE-22)

  CWE-22 — Path Traversal
  https://cwe.mitre.org/data/definitions/22.html

  function:    serveFile
  type:        security
  score:       13
  confidence:  high
  callers:     3
  entry:       GET /download  (1 hop)
  coverage:    0 tests

  ── Fix ──────────────────────────────────────────────────────────
  Use `filepath.Join` + `filepath.Clean` and verify the resolved
  path starts with the base directory

  joined := filepath.Join(baseDir, segment)
  if !strings.HasPrefix(filepath.Clean(joined), filepath.Clean(baseDir)) {
      return errors.New("path traversal attempt")
  }

  ── Call sites ───────────────────────────────────────────────────
  src/middleware/download.go:89
  src/handlers/static.go:112
  src/admin/export.go:201
  ══════════════════════════════════════════════════════════════════

AXIOM auto-detects the project root by walking up from the target file looking for .git, package.json, go.mod, Cargo.toml, pyproject.toml, or Gemfile. Pass --dir to override.


Fix suggestions in terminal output

Every finding that has a suggested fix shows it inline in the terminal report. The fix → line appears in green:

  ● CRITICAL  src/db/query.go:77
    SQL query built with string concatenation in `Query()` — susceptible to SQL injection
    ├─ relied on by 5 call sites
    ├─ nearest public entry: GET /search  (2 hops)
    ├─ tests covering null path: 0
    └─ fix → Use `?` or `$N` placeholders and pass values as separate arguments
         rows, err := db.Query("SELECT ... WHERE id = ?", id)

Fix snippets are also included in JSON output under suggested_fix.snippet.


Concurrency patterns

Go — mu.Lock() without defer mu.Unlock(); wg.Add() without wg.Wait()

Java — lock.lock() without finally { unlock() }; executor.submit() without shutdown()

Python — lock.acquire() without finally: lock.release() (prefer with lock:); Thread.start() without Thread.join()

C# — Monitor.Enter() without finally { Monitor.Exit() }; SemaphoreSlim.Wait() without finally { Release() }; Task.Run() result discarded (fire-and-forget)

Ruby — Thread.new without .join; mutex.lock without .unlock (prefer mutex.synchronize {})

Rust — let _ = mutex.lock() (guard dropped immediately, lock released at end of statement); thread::spawn handle not joined


Inter-procedural resource tracking

Resource analysis crosses function boundaries. If a function wraps os.Open and returns the handle, every call site that doesn't close the returned value is flagged:

// opener.go
func openConfig(path string) *os.File {
    f, _ := os.Open(path)
    return f               // ← marked as resource-returning wrapper
}

// handler.go
func handleRequest(path string) {
    f := openConfig(path)  // ← FLAGGED: `defer f.Close()` missing
    parseConfig(f)
}

Wrapper functions are not flagged themselves. Two-hop chains are resolved (openRaw → openWrapped → caller). Works across files in the same scan.


Languages

| Language | Parser | |----------|--------| | TypeScript / JavaScript | @typescript-eslint/typescript-estree | | Python | tree-sitter (WASM) | | Go | tree-sitter (WASM) | | Ruby | tree-sitter (WASM) | | Java | tree-sitter (WASM) | | Rust | tree-sitter (WASM) | | C# | tree-sitter (WASM) | | PHP | tree-sitter (WASM) |


Usage

# Scan current directory
axiom scan

# Scan a specific path
axiom scan ./src

# JSON output for CI pipelines
axiom scan --json > axiom-report.json

# SARIF output for GitHub code scanning
axiom scan --sarif > results.sarif

# Only show critical and high severity
axiom scan --min-severity high

# Exit with code 1 if any critical finding exists
axiom scan --fail-on critical

# Only scan files changed since a branch or commit
axiom scan --since main
axiom scan --since HEAD~5

# Watch mode
axiom scan --watch

# Explain a specific invariant with full remediation card
axiom explain src/billing.ts:47

# Explain against a specific project directory
axiom explain src/billing.ts:47 --dir /path/to/project

Inline suppressions

Add axiom-ignore on the flagged line or the line above:

// axiom-ignore
const plan = user.subscription.plan;

Works in all supported languages using that language's comment syntax.


LSP server

axiom-lsp is included. It surfaces findings as diagnostics in VS Code, Neovim, Emacs, and any LSP-compatible editor with no extension required. Validates on file open and save.

Neovim (nvim-lspconfig):

require('lspconfig').axiom.setup({
  cmd = { 'axiom-lsp', '--stdio' },
  filetypes = { 'typescript', 'javascript', 'python', 'go', 'ruby', 'java', 'rust', 'cs', 'php' },
})

Ranking

Each invariant is scored by blast radius:

score = (call_sites × 2)
      + entrypoint_proximity   // closer to HTTP handler = higher score
      + type_weight            // security=10, concurrency=6, null=3, resource=3, ordering=2, swallowing=2, shape=1
      - test_coverage_discount

Security invariants start at type_weight=10, so even a finding with zero call sites and no entry point scores 11 → HIGH minimum.

Severity buckets: CRITICAL (>20) · HIGH (10–20) · MEDIUM (5–10) · LOW (<5)


Configuration

Create .axiomrc.json in your project root:

{
  "ignore": ["**/*.generated.ts", "src/migrations/**"],
  "minScore": 5,
  "maxResults": 100,
  "patterns": {
    "nullMethods": ["findBySlug", "fetchLatest"],
    "nullFunctions": ["my_custom_fetch"],
    "lifecycleMethods": ["onBoot", "warmUp"],
    "assertionFunctions": ["invariant", "ensure", "checkNotNull"]
  }
}

| Field | Description | |-------|-------------| | ignore | Glob patterns to exclude | | minScore | Minimum blast-radius score to include | | maxResults | Cap total results (highest score first) | | patterns.nullMethods | Extra method names that return null/nil/undefined | | patterns.nullFunctions | Extra function names that return null | | patterns.lifecycleMethods | Extra methods treated as secondary constructors for ordering checks | | patterns.assertionFunctions | Functions that guarantee non-null (invariant, assert, etc.) |


CI / GitHub Actions

- name: Run AXIOM
  run: npx axiom-scan scan --sarif > axiom.sarif

- name: Upload to GitHub Code Scanning
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: axiom.sarif

# Gate on critical findings
- name: Fail on critical
  run: npx axiom-scan scan --fail-on critical

Diff mode for PRs — only scan changed files:

- name: AXIOM diff scan
  run: npx axiom-scan scan --since origin/main --fail-on critical

Why not...

| Tool | Gap | |------|-----| | Istanbul / V8 coverage | Measures line execution, not behavioral assumptions | | TypeScript strict mode | Catches declared-type nulls only, not behavioral invariants | | ESLint / RuboCop / golangci-lint | Rule-based — you write the rules; AXIOM infers them | | Semgrep / CodeQL | Pattern matching — you write the patterns; AXIOM infers them | | Mutation testing | Slow, requires tests to exist, doesn't find untested assumptions | | Bandit / Brakeman | Security-only, single-language; AXIOM covers behavioral invariants across 8 languages in the same pass |


License

MIT