mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-27 20:29:39 +02:00
Phase 1 (#33)
* 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:
parent
19b578c5c4
commit
1bbe4b1cfb
456 changed files with 25628 additions and 1228 deletions
35
tests/fixtures/real_world/javascript/taint/cmdi_express.expect.json
vendored
Normal file
35
tests/fixtures/real_world/javascript/taint/cmdi_express.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
tests/fixtures/real_world/javascript/taint/cmdi_express.js
vendored
Normal file
18
tests/fixtures/real_world/javascript/taint/cmdi_express.js
vendored
Normal 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);
|
||||
});
|
||||
});
|
||||
36
tests/fixtures/real_world/javascript/taint/eval_user_input.expect.json
vendored
Normal file
36
tests/fixtures/real_world/javascript/taint/eval_user_input.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
tests/fixtures/real_world/javascript/taint/eval_user_input.js
vendored
Normal file
17
tests/fixtures/real_world/javascript/taint/eval_user_input.js
vendored
Normal 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 });
|
||||
});
|
||||
36
tests/fixtures/real_world/javascript/taint/path_traversal_fs.expect.json
vendored
Normal file
36
tests/fixtures/real_world/javascript/taint/path_traversal_fs.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
tests/fixtures/real_world/javascript/taint/path_traversal_fs.js
vendored
Normal file
20
tests/fixtures/real_world/javascript/taint/path_traversal_fs.js
vendored
Normal 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);
|
||||
});
|
||||
35
tests/fixtures/real_world/javascript/taint/proto_pollution.expect.json
vendored
Normal file
35
tests/fixtures/real_world/javascript/taint/proto_pollution.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
tests/fixtures/real_world/javascript/taint/proto_pollution.js
vendored
Normal file
19
tests/fixtures/real_world/javascript/taint/proto_pollution.js
vendored
Normal 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);
|
||||
});
|
||||
24
tests/fixtures/real_world/javascript/taint/sqli_concat.expect.json
vendored
Normal file
24
tests/fixtures/real_world/javascript/taint/sqli_concat.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
tests/fixtures/real_world/javascript/taint/sqli_concat.js
vendored
Normal file
14
tests/fixtures/real_world/javascript/taint/sqli_concat.js
vendored
Normal 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;
|
||||
});
|
||||
}
|
||||
36
tests/fixtures/real_world/javascript/taint/xss_res_send.expect.json
vendored
Normal file
36
tests/fixtures/real_world/javascript/taint/xss_res_send.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
tests/fixtures/real_world/javascript/taint/xss_res_send.js
vendored
Normal file
13
tests/fixtures/real_world/javascript/taint/xss_res_send.js
vendored
Normal 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>';
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue