* chore: Exclude CLAUDE.md from Cargo.toml

* feat: add callgraph module and integrate into main analysis flow

* feat: enhance CLI with new severity filtering and analysis modes

* feat: update CHANGELOG with recent enhancements and fixes to severity filtering and output handling

* feat: implement state-model dataflow analysis for resource lifecycle and auth state

* feat: enhance diagnostic output formatting and add evidence structure

* feat: implement attack surface ranking for diagnostics with scoring and sorting

* feat: add comprehensive documentation for installation, usage, and rules reference

* feat: add multiple language support for command execution and evaluation endpoints

* feat: implement inline suppression for findings using `nyx:ignore` comments

* feat: add confidence levels to AST patterns and update output structure

* feat: implement low-noise prioritization system with category filtering, rollup grouping, and configurable budgets

* feat: bump version to 0.4.0 and update changelog with new features and improvements

* feat: add dead code allowances to various functions in mod.rs and real_world_tests.rs
This commit is contained in:
Eli Peter 2026-02-25 21:16:36 -05:00 committed by GitHub
parent 19b578c5c4
commit 1bbe4b1cfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
456 changed files with 25628 additions and 1228 deletions

View file

@ -0,0 +1,35 @@
{
"description": "Express req.query flows into child_process.exec (command injection). Safe version uses regex replace but scanner lacks custom sanitizer recognition.",
"tags": [
"taint",
"cmdi",
"express"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
4,
10
],
"evidence_contains": [],
"notes": "req.query.host flows directly into child_process.exec via string concatenation"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
12,
19
],
"evidence_contains": [],
"notes": "Safe version still fires because .replace is not a recognized sanitizer for SHELL_ESCAPE cap"
}
]
}

View file

@ -0,0 +1,18 @@
var child_process = require('child_process');
var express = require('express');
var app = express();
app.get('/ping', function(req, res) {
var host = req.query.host;
child_process.exec('ping -c 1 ' + host, function(err, stdout) {
res.send(stdout);
});
});
app.get('/safe-ping', function(req, res) {
var host = req.query.host;
var sanitized = host.replace(/[^a-zA-Z0-9.]/g, '');
child_process.exec('ping -c 1 ' + sanitized, function(err, stdout) {
res.send(stdout);
});
});

View file

@ -0,0 +1,36 @@
{
"description": "eval() with user-controlled input from req.query. AST pattern detects eval call; taint detects source-to-sink flow.",
"tags": [
"taint",
"code-exec",
"eval",
"express"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "js.code_exec.eval",
"severity": null,
"must_match": true,
"line_range": [
4,
8
],
"evidence_contains": [],
"notes": "AST pattern matches eval() call regardless of arguments"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
3,
8
],
"evidence_contains": [],
"notes": "req.query.expr flows directly into eval() which is a SHELL_ESCAPE sink"
}
]
}

View file

@ -0,0 +1,17 @@
var express = require('express');
var app = express();
app.get('/calc', function(req, res) {
var expr = req.query.expr;
var result = eval(expr);
res.json({ result: result });
});
app.get('/calc-safe', function(req, res) {
var expr = req.query.expr;
var num = parseFloat(expr);
if (isNaN(num)) {
return res.status(400).send('Invalid');
}
res.json({ result: num });
});

View file

@ -0,0 +1,36 @@
{
"description": "Path traversal via req.query flowing into fs.readFileSync. Scanner lacks fs.readFileSync as a defined sink.",
"tags": [
"taint",
"path-traversal",
"express",
"fs"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": false,
"line_range": [
5,
10
],
"evidence_contains": [],
"notes": "req.query.path flows into fs.readFileSync but fs.readFileSync is not a recognized taint sink"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": false,
"line_range": [
11,
20
],
"evidence_contains": [],
"notes": "Safe version uses path.resolve and startsWith guard; would require adding fs sinks to detect"
}
]
}

View file

@ -0,0 +1,20 @@
var express = require('express');
var fs = require('fs');
var path = require('path');
var app = express();
app.get('/read', function(req, res) {
var filePath = req.query.path;
var content = fs.readFileSync(filePath, 'utf8');
res.send(content);
});
app.get('/read-safe', function(req, res) {
var filePath = req.query.path;
var resolved = path.resolve('/safe/dir', filePath);
if (!resolved.startsWith('/safe/dir')) {
return res.status(403).send('Forbidden');
}
var content = fs.readFileSync(resolved, 'utf8');
res.send(content);
});

View file

@ -0,0 +1,35 @@
{
"description": "Prototype pollution via recursive merge of user-controlled req.body. The __proto__ assignment is indirect (dynamic key).",
"tags": [
"taint",
"prototype-pollution",
"express"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "js.prototype.proto_assignment",
"severity": null,
"must_match": false,
"line_range": [
2,
8
],
"evidence_contains": [],
"notes": "Dynamic property assignment target[key] could pollute __proto__ but AST pattern only matches literal __proto__ property access"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": false,
"line_range": [
13,
19
],
"evidence_contains": [],
"notes": "req.body flows through merge into config but no recognized sink is reached"
}
]
}

View file

@ -0,0 +1,19 @@
function merge(target, source) {
for (var key in source) {
if (typeof source[key] === 'object') {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
var express = require('express');
var app = express();
app.post('/config', function(req, res) {
var defaults = { theme: 'light', lang: 'en' };
var config = merge(defaults, req.body);
res.json(config);
});

View file

@ -0,0 +1,24 @@
{
"description": "SQL injection via string concatenation with userId parameter. connection.query is not a recognized taint sink.",
"tags": [
"taint",
"sqli",
"mysql"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": false,
"line_range": [
2,
7
],
"evidence_contains": [],
"notes": "userId flows into SQL string via concat, but connection.query is not a defined sink and userId as a function param is not auto-tainted"
}
]
}

View file

@ -0,0 +1,14 @@
var mysql = require('mysql');
function getUser(connection, userId) {
var query = 'SELECT * FROM users WHERE id = ' + userId;
connection.query(query, function(err, results) {
return results;
});
}
function getUserSafe(connection, userId) {
connection.query('SELECT * FROM users WHERE id = ?', [userId], function(err, results) {
return results;
});
}

View file

@ -0,0 +1,36 @@
{
"description": "XSS via req.query flowing into innerHTML. DOMPurify.sanitize is a recognized HTML_ESCAPE sanitizer.",
"tags": [
"taint",
"xss",
"express",
"innerHTML"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
3,
8
],
"evidence_contains": [],
"notes": "req.query.name flows into innerHTML via string concatenation"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": false,
"line_range": [
8,
14
],
"evidence_contains": [],
"notes": "DOMPurify.sanitize strips HTML_ESCAPE cap so this should NOT fire; must_match=false means we expect absence"
}
]
}

View file

@ -0,0 +1,13 @@
var express = require('express');
var app = express();
app.get('/greet', function(req, res) {
var name = req.query.name;
document.getElementById('header').innerHTML = '<h1>Hello ' + name + '</h1>';
});
app.get('/greet-safe', function(req, res) {
var name = req.query.name;
var clean = DOMPurify.sanitize(name);
document.getElementById('header').innerHTML = '<h1>Hello ' + clean + '</h1>';
});