`Part 2 of the quantum-audit series. Part 1 here
🌐 Tool: quantum-audit-site.vercel.app
When I published quantum-audit — a CLI that scans npm projects for quantum-vulnerable cryptography — I got a question immediately:
"How does it actually detect crypto calls in source code?"
The answer is AST scanning. In this post I'll break down exactly how it works, with real code from the scanner.
What is an AST?
AST stands for Abstract Syntax Tree. It's a tree representation of your source code that a parser produces after reading it.
Think of it like this. When you write:
js
crypto.createSign('RSA-SHA256')
To you, that's a function call. To a parser, it's a tree:
plaintext
CallExpression
├── callee: MemberExpression
│ ├── object: Identifier (name: "crypto")
│ └── property: Identifier (name: "createSign")
└── arguments:
└── StringLiteral (value: "RSA-SHA256")
Instead of using regex — which would miss edge cases and produce false positives — we walk this tree and look for specific patterns. That's the core idea.
Why not just use regex?
You could try:
js
const hasRSA = code.includes('createSign') && code.includes('RSA');
But this breaks immediately on:
js
// createSign is used for RSA-SHA256
const comment = "createSign";
Regex doesn't understand code structure. An AST does.
The tools: @babel/parser and @babel/traverse
quantum-audit uses two packages:
-
@babel/parser— reads JavaScript/TypeScript and produces an AST -
@babel/traverse— walks the AST and lets you hook into specific node types
bash
npm install @babel/parser @babel/traverse
Reading the file and parsing it
The first step is reading the source file and converting it into an AST:
`js
const fs = require('fs');
const parser = require('@babel/parser');
function scanSourceFile(filePath) {
const findings = [];
let code;
try {
code = fs.readFileSync(filePath, 'utf8');
} catch {
return findings; // skip unreadable files
}
let ast;
try {
ast = parser.parse(code, {
sourceType: 'unambiguous', // handles both CommonJS and ES modules
plugins: ['jsx', 'typescript'], // supports React and TypeScript
});
} catch {
return findings; // skip files that fail to parse
}
`
sourceType: 'unambiguous' tells the parser to figure out automatically whether the file uses import/export or require(). The plugins array enables JSX and TypeScript support so the scanner works on React and TS projects.
Walking the tree with traverse
Once we have the AST, we use traverse to walk every node and check for dangerous patterns:
`js
const traverse = require('@babel/traverse').default;
traverse(ast, {
CallExpression(path) {
const callee = path.node.callee;
const line = path.node.loc ? path.node.loc.start.line : null;
// Check for: crypto.createSign('RSA-SHA256')
if (
callee.type === 'MemberExpression' &&
callee.property.name === 'createSign'
) {
const arg = path.node.arguments[0];
if (arg && arg.type === 'StringLiteral') {
if (/RSA/i.test(arg.value)) {
findings.push({
algorithm: 'RSA (crypto.createSign)',
risk: 'critical',
weight: 35,
file: filePath,
line,
});
}
}
}
}
});
`
CallExpression is the node type for any function call. Every time traverse finds one, our callback fires. We then inspect the node to see:
- Is the callee a
MemberExpression(i.e.something.method())? - Is the method name
createSign? - Is the first argument a string containing
RSA?
If all three are true — we've found RSA signing code. Flag it.
Detecting all the vulnerable patterns
Here's the full set of patterns quantum-audit looks for:
RSA and DSA via createSign
js
crypto.createSign('RSA-SHA256') // CRITICAL
crypto.createSign('DSA-SHA1') // CRITICAL
Detection:
js
if (callee.property.name === 'createSign') {
const arg = path.node.arguments[0];
if (arg && arg.type === 'StringLiteral') {
if (/RSA/i.test(arg.value)) {
findings.push({ algorithm: 'RSA (crypto.createSign)', risk: 'critical', weight: 35, file: filePath, line });
} else if (/DSA/i.test(arg.value)) {
findings.push({ algorithm: 'DSA (crypto.createSign)', risk: 'critical', weight: 35, file: filePath, line });
}
}
}
ECDSA and RSA via generateKeyPair
js
crypto.generateKeyPairSync('ec', { namedCurve: 'secp256k1' }) // CRITICAL
crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }) // CRITICAL
Detection:
js
if (/generateKeyPair/.test(callee.property.name || '')) {
const arg = path.node.arguments[0];
if (arg && arg.type === 'StringLiteral') {
if (arg.value === 'rsa') {
findings.push({ algorithm: 'RSA (generateKeyPair)', risk: 'critical', weight: 35, file: filePath, line });
} else if (arg.value === 'ec') {
findings.push({ algorithm: 'ECDSA (generateKeyPair)', risk: 'critical', weight: 40, file: filePath, line });
}
}
}
SHA-256 via createHash (medium risk)
js
crypto.createHash('sha256') // MEDIUM — weakened by Grover's algorithm
Detection:
js
if (callee.property.name === 'createHash') {
const arg = path.node.arguments[0];
if (arg && arg.type === 'StringLiteral' && /^sha256$/i.test(arg.value)) {
findings.push({ algorithm: 'SHA-256 (crypto.createHash)', risk: 'medium', weight: 8, file: filePath, line });
}
}
SHA-256 isn't broken by Shor's algorithm — but Grover's algorithm halves its effective security from 256 bits to ~128 bits. It's a medium risk, not critical.
Walking the entire project
The scanner doesn't just check one file. It walks your entire project directory recursively, skipping node_modules, dist, and other non-source folders:
`js
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage']);
function walkSourceFiles(dir, fileList = []) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name)) continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkSourceFiles(fullPath, fileList);
} else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
fileList.push(fullPath);
}
}
return fileList;
}
`
Every .js, .ts, .jsx, .tsx, .mjs, and .cjs file gets scanned.
Two layers working together
The AST scanner is Layer 2. Layer 1 is a dependency scan — checking your package.json against a database of known vulnerable libraries:
js
// known-libs.js — excerpt
module.exports = {
'elliptic': { algorithm: 'ECDSA (secp256k1/p256)', risk: 'critical', weight: 40 },
'ethers': { algorithm: 'ECDSA (secp256k1) signing', risk: 'critical', weight: 40 },
'node-rsa': { algorithm: 'RSA', risk: 'critical', weight: 35 },
};
The dependency scan catches high-level exposure. The AST scan catches the specific lines in your source where vulnerable crypto is called directly.
Together they produce a score:
plaintext
Score = 100 - (sum of weights of all findings)
With deduplication so the same finding doesn't penalise you twice.
Try it on your project
bash
npx quantum-audit .
npx quantum-audit . --json # machine-readable for CI
GitHub: takundanashebmuchena-pixel/quantum-audit
npm: npmjs.com/package/quantum-audit
What patterns are you seeing in your own codebase? Drop your score in the comments.`
Top comments (0)